WeRateDogs是推特帐最受欢迎的搞笑萌宠推主之一。其开创性的将宠物,打分和搞笑元素结合,收获了不少粉丝。我们拿到了与WeRateDogs相关的三个数据集,尝试探索15年11月到17年8月之推特账号所蕴藏的,粉丝的喜好的秘密。我们修复了原始数据集存在的13个质量问题,两个整洁度问题,并额外提取了4项特征以便分析。在观察了推特账号的运营情况后进行进一步探索的过程中,我们确认发帖时间,发帖时是星期几,推文主体的性别,以及推文主体究竟是不是狗,很可能对单条推文的受欢迎程度几乎没有影响。我们对以下三个方面进行了更加深入的探究:品种,评分和生长分类。我们最终的结论可以用以下三句话概括:1)众汪平等:人们对不同品种的狗没有特别的偏好;2)小奶狗不吃香:WeRateDogs推特中小狗的受欢迎程度比不上大狗;3)低分有铁粉,高分更吸睛:尽管高评分的推特更有可能获得更高的点赞量,但地评分的推文反而更容易获得转发。最后,我们探讨了上述的三点观察背后可能的原因,并指出了本文的不足和后续努力的方向。
WeRateDogs是推特上受欢迎的萌宠推主之一。
其主要风格为搞笑,具体表现形式为使用推文为一张狗狗(well,理论上都是)的照片进行十分制的打分。尽管其满分为10分,但推主常常打出10分以上的高分(13,14等),并一本正经的配上推文说明理由,十分有趣。推主有时也会一本正经给一些并不是狗的照片打分,喜剧效果十足。某些时候某些狗狗会被刻意打出低分,同样配上令人捧腹的推文。
WeRateDogs创建于11月15日。创始人时坎贝尔大学的(辍)学生Matt Nelson,当时读该校高尔夫管理专业的大二学生。当时,他和他的朋友们一起在一家苹果蜜蜂餐厅(Applebee's)的餐桌旁,用WeRateDogs账号发出了第一条推特。
如同天底下所有真实的创业故事那样,这个独特的推特账号的发展并非一帆风顺。15年11、12月是最初的激情,很快挑战便接踵而至:
身心俱疲的Nelson当然想过放弃。他联系了John Ricci,一位拥有丰富社交媒体运营经验的市场人,想要卖掉账号一了百了,没想到后者居然拒绝了。Rucci看到了账号的潜力,极力劝说Matt改变方式坚持运营,并在16年某个未知的时间点开始帮助他运营。Rucci每天帮Matt筛选出20-30张各地粉丝发来的请求打分的照片,而Nelson则挑出两张,分别在中午和晚上8:00左右发出即可。慢慢的,Nelson重新找到了节奏。17年某个未知的时间点,升入大四的Nelson退学,全心投入推特账号的运营。
现在,WeRateDogs主要依靠自营电商盈利,向通过推特账号积攒的粉丝售卖写有推特账号标志性语录的狗狗围巾、卫衣等。Nelson自己透露,每个月的收入在1万美元左右。
综合自下列报道
显然,WeRateDogs误打误撞探索出的这一定位十分有趣。传统上,萌宠推主千千万万,但绝大多数萌宠的推文都停留在“啊!好萌!”的层次上,而萌宠推主们之间的竞争主要依靠谁更萌获胜,这显然同质化严重而且很容易相互抄袭;给狗狗评价和打分的博主又太过严肃显得不近人情;而传统的搞笑博主又几乎没有与宠物结合起来的。系统性的发掘狗狗的萌点,并通过带有喜剧色彩的文案强化,最后配上打破传统令人捧腹的打分和评价,令人耳目一新并不令人意外。
但是,正如开心麻花的喜剧,在让观众开怀大笑之余,也有商业和受欢迎程度的考量一样,WeRateDogs的内容质量显然也是有部分更受欢迎的。问题是,究竟是怎样的内容更受欢迎呢?为了更好的云吸狗,我选择了这个非常繁琐,非常容易导致过度分析(事实上确实如此),看起来也不甚有意义的角度进行了探索。甚至,在探索的过程中,我的腹肌似乎因为笑得过于猛烈而受到了伤害。
在进行分析之前,我们需要明确,什么是“更受欢迎”。传统意义上,我们需要知道一条推特在发出的时候有多少关注者,这条推特有多少浏览量,多少点赞数和多少转发数,来一步一步计算转化比例。在这一框架中,更多的浏览量可能意味着火爆,但运营者们显然更关注点赞和转发的数据。
在这篇过度分析中,我们将围绕两个维度进行受欢迎程度的探索:
转赞比可以用来衡量核心用户占比的理论依据如下:
我们将围绕以下数据集展开探索:
我们将首先惊醒项目初始化(不然呢);随后将进行数据集的初步探索,并根据需要清洗数据,拓展数据集特征;随后,我们将简单的使用核心数据素描一下WeRateDogs账号;最后,我们将针对如下特性展开点赞量和转赞比区别的探索,并尝试得出结论:
# 导入项目基础依赖包
import re
import requests
import datetime
import numpy as np
import pandas as pd
from wordcloud import WordCloud, STOPWORDS, ImageColorGenerator
# 设定pandas dataframe的显示宽度,防止折叠
# 参考:https://ask.csdn.net/questions/367253
pd.set_option('display.max_colwidth', 1000)
# 设置pandas dataframe的最大行数,显著节约空间,减少割裂感
# 参考:https://stackoverflow.com/questions/42504984/python-pandas-select-both-head-and-tail
# 参考:https://pandas.pydata.org/pandas-docs/stable/generated/pandas.set_option.html
pd.set_option('display.max_rows', 10)
# 统计学相关包
import scikit_posthocs as sp
import scipy.stats as stats
import statsmodels.api as sm
from statsmodels.formula.api import ols
from statsmodels.stats.multicomp import pairwise_tukeyhsd
from statsmodels.stats.multicomp import MultiComparison
# 导入项目可视化部分依赖包
import matplotlib.pyplot as plt
import seaborn as sns
# plotLy需要1.9以上的版本
import plotly.offline as of
import plotly.graph_objs as go
of.init_notebook_mode(connected=True)
from matplotlib.colors import ListedColormap
%matplotlib inline
sns.set(style='white')
# 获取文件现在的路径,用于文件储存
import os
os.getcwd()
# 导入展示图片所需的库
from scipy.misc import imread
from PIL import Image
from io import BytesIO
# 设定可视化颜色-分类类别
# 主色调
ui = ["#01B8AA", "#374649", "#FD625E", "#F2C80F", "#5F6B6D", "#8AD4EB", "#FE9666", "#A66999", "#3599B8", "#DFBFBF"]
# 浅一阶
ui_light = ["#4AC5BB", "#5F6B6D", "#FB8281", "#F4D25A","#7F898A", "#A4DDEE", "#FDAB89", "#B687AC", "#28738A", "#A78F8F"]
# 深一阶
ui_dark = ["#168980", "#293537", "#BB4A4A", "#B59525", "#475052", "#6A9FB0", "#BD7150", "#7B4F71", "#1B4D5C", "#706060"]
# 将颜色设定为Seaborn Palette
ui_palette_light = sns.color_palette(ui_light)
ui_palette_dark = sns.color_palette(ui_dark)
ui_palette_default = sns.set_palette(ui)
# 主色调预览
sns.palplot(sns.color_palette(ui))
# 设置可视化颜色-连续类别
# 内心os:Seaborn实乃神器也哇哈哈哈哈~
sequential_ui = sns.dark_palette(
"#82FFF5", n_colors=20, reverse=True, as_cmap=True)
sequential_ui_palette_reverse = sns.dark_palette(
"#82FFF5", n_colors=20, reverse=False)
sns.palplot(sns.dark_palette("#82FFF5", n_colors=20, reverse=True))
# 加载包含狗狗信息的原始数据集,并命名为“dogrates"
dogrates = pd.read_csv('twitter-archive-enhanced.csv', encoding='utf8')
# 加载包含了预测狗狗品种的数据集,并命名为breeds
# 根据要求,使用request库进行下载
url = "https://raw.githubusercontent.com/udacity/new-dand-advanced-china/master/%E6%95%B0%E6%8D%AE%E6%B8%85%E6%B4%97/WeRateDogs%E9%A1%B9%E7%9B%AE/image-predictions.tsv"
image_perdictions = requests.get(url)
# 将网络上的返回保存为文件
# 为啥requests库就不把这个部分搞简单点呢~瞧瞧人家urllib,一个retrieve就搞定了~
with open('image-predictions-download.tsv', mode='wb') as downloads:
downloads.write(image_perdictions.content)
breeds = pd.read_csv('image-predictions-download.tsv', sep='\t', encoding = 'utf8')
# 加载推特转载数记录的数据集,并将其命名为retweet
# 由于众所周知的原因,没有通过Twitter API进行下载;这里我们直接导入优达学城提供的数据
retweets = pd.read_json('tweet.json', orient='string',
lines=True, encoding='utf8')
dogrates.head(1)
breeds.head(1)
retweets.head(1)
这些数据集看起来包含了许多我们不需要的信息;在如此之多的无效信息面前,我们无法做出有效的判断。因此,在评估数据质量之前,我们需要去除我们不需要的列,以减少信息干扰。
dogrates.columns
breeds.columns
retweets.columns
经观察,dogrates和breeds两个数据集都有tweet_id列,而retweets数据集则有id列,可以作为键值进行数据的匹配;而retweets数据集中id_str与tweet_id列不匹配,无需包含。经过精简的数据集将被命名为dogratesLite,以与原数据集区分。
dogrates数据集中,保留tweet_id、timestamp, text, rating_numerator, rating_denominator, name, doggo, floofer, puper, 和puppo列;因为项目文件中提及数据集中包含转发的推特数据,因此同样保留retweeted_status_id列,以便进一步观察;经过精简的dogrates数据集将被命名为dogrates_lite,以与原数据集区分。
# code
dogrates_lite = dogrates[['tweet_id', 'timestamp', 'text', 'rating_numerator',
'rating_denominator', 'name', 'doggo', 'floofer', 'pupper', 'puppo', 'retweeted_status_id']]
# test
dogrates_lite.head(2)
breeds数据集中,除必须的tweet_id和jpg_url外,我们只需要预测最准确的图片所预测的品种p1 ,作为该图片最主要的特征;此外,我们需要全部预测该图片的分类是否为狗的列,包括p1_dog,p2_dog,p3_dog;经过精简的breeds数据集将被命名为breeds_lite,以与原数据集区分。
# code
breeds_lite = breeds[['tweet_id', 'jpg_url', 'p1', 'p1_dog', 'p2_dog', 'p3_dog']]
# test
breeds_lite.head(2)
# test for duplicated
breeds_lite['jpg_url'].duplicated().value_counts()
retweets数据集中,只保留id, favorite_count, retweet_count三列;经过精简的retweets数据集将被命名为retweets_lite,以与原数据集区分。
# code
retweets_lite = retweets[['id', 'favorite_count', 'retweet_count']]
# test
retweets_lite.head(2)
因为我们将要回答的问题,必须同时在已经导入的三个数据集中都有对应的数据。任何缺失都将导致分析的结果没有意义。
为了避免不必要的清洗操作,我们现在就用dogrates_lite数据集中的tweet_id列匹配breeds_lite中的tweet_id和retweets_lite数据集中的id,过滤掉不必要的行。
最后,dogrates_lite数据集将被重新赋值为过滤后的数据集dogrates_lite数据集。
# 为了更灵活的筛选数据集之间的数据,我们定义如下函数:
def ISIN(df1, df2, column, df3=None, logic=None, mismatch=False):
'''我们定义ISIN函数,其实为.isin方法针对本次研究的封装;
其必须传入两个数据集的名称: df1和df2,和匹配二者的键值的列名称column(必须是字符串);其中df1是主数据集;
其可以传入第三个数据集df3的名称,搭配logic参数(必须是字符串)来进行多个数据集间数据的匹配,可选逻辑为“&”和“|”;匹配依旧基于column;
其可以传入mismatch参数,意为是否取df1中df1与df2的差集;必须是布尔值,默认为False;为False时默认取函数交集;
当mismatch为True时,若只有两个数据集传入,则默认取df1不在df2中的数据集;
若有三个数据集传入,则默认只取df1与df3的差集
'''
if mismatch == False:
if logic == '&':
new_df = df1[(df1[column].isin(df2[column])) &
(df1[column].isin(df3[column]))]
return new_df
elif logic == '|':
new_df = df1[(df1[column].isin(df2[column])) |
(df1[column].isin(df3[column]))]
return new_df
else:
new_df = df1[(df1[column].isin(df2[column]))]
return new_df
elif mismatch == True:
if logic == '&':
new_df = df1[(df1[column].isin(df2[column])) &
~(df1[column].isin(df3[column]))]
return new_df
elif logic == '|':
new_df = df1[(df1[column].isin(df2[column])) |
~(df1[column].isin(df3[column]))]
return new_df
else:
new_df = df1[~(df1[column].isin(df2[column]))]
return new_df
else:
print("This computer is exploding in 5, 4, 3, 2, 1...")
dogrates_dropped = ISIN(dogrates_lite, breeds_lite, 'tweet_id', mismatch=True)
dogrates_dropped.info()
# 去掉不必要的行
dogrates_lite = ISIN(dogrates_lite, breeds_lite, 'tweet_id')
dogrates_lite.info()
# 查看数据集
dogrates_lite
dogrates_lite.info()
# 没有空行,✓
# timestamp数据类型不正确,应为datetime
# doggo, floofer, pupper, puppo列有缺失值
# doggo, floofer, pupper, puppo应为一列标明种类(整洁度问题)
dogrates_lite.describe()
# numerator有明显超出正常值的最大值,稍后需要使用value_counts方法检查;最小值为0也不正确;正常应为10-20之内的整数;
# denominator平均值不为10,也有远远超过正常值的最大值;最小值为0也不正确;正常应全部为10;
dogrates_lite['rating_numerator'].value_counts()
# 低于10分的分数都需要进行调差
# 高于20的分数都需要进行调查
dogrates_lite['rating_denominator'].value_counts()
dogrates_lite['name'].value_counts()
retweeted_id_notnull = dogrates_lite[dogrates_lite['retweeted_status_id'].notnull()]
retweeted_id_notnull
breeds_lite
breeds_lite.info()
breeds_lite.describe()
retweets_lite数据集的评估¶retweets_lite
# retweets_lite数据集中id列的名字需改为与之前一致即可
retweets_lite.info()
retweets_lite.describe()
常见的数据质量问题包括:
我们将从上述4个方面入手,总结我们观察到的一系列问题:
dogrates_lite数据集
dogrates_lite['name'].value_counts().head()
dogrates_lite数据集
rating_denominator列不为10:¶# 516, 1068, 1165, 1202, 1662, 2335
ix_wrong_rates = [1068, 1165, 1202, 1662, 2335]
tweets_multiple_number = dogrates_lite.loc[ix_wrong_rates]
tweets_multiple_number
上述数据集是根据视觉检视后直接筛选的。有没有可能有遗漏呢?当然有。我们将在2.2.1.5小节中做进一步的探讨。
# 祝狗狗早日康复,但我们还是得准备把这条删掉
tweets_fund_raise = dogrates_lite.loc[[516]]
tweets_fund_raise
rating_denominator列不为10:¶tweets_multiple_dogs = dogrates_lite[
(dogrates_lite['rating_denominator'] != 10)
& (~dogrates_lite.index.isin(ix_wrong_rates))
]
tweets_multiple_dogs
# 此大块的目的是:筛选出所有不与分母异常重复的分子异常的行,与品种交叉对比并最终决定后续处理
# 此块目的:筛选出分子为异常的行,并排除已经在分母异常中筛选出的行,以便后续筛选;是大块中最基础的部分
# -----------------------------
# 将分子数值的计数保存为dataframe,并为表头重命名一方便检索
rating_counts = dogrates_lite['rating_numerator'].value_counts(
).to_frame().reset_index()
rating_counts.rename(columns={'index': 'numerator',
'rating_numerator': 'value_counts'}, inplace=True)
# 因异常值出现的概率普遍较小,所以筛选出小于2的值作为潜在异常值
# 将潜在异常值另存为一个名为rating_counts1的数据集,方便后续根据rating_numerator列的值筛选出可能包含异常值的行
# 如果数据已经没有多只狗狗一起打分的可能性,那么分数只有大于20才应被认为是异常
rating_counts1 = rating_counts[(rating_counts['value_counts'] < 2) & (
rating_counts['numerator'] > 20)]
# 匹配潜在异常值
# 筛选出dogrates_lite数据集中,在rating_counts1里,又不在上述分母问题集中的rating_numerator值
tweets_wrong_numerator = dogrates_lite[
(dogrates_lite['rating_numerator'].isin(rating_counts1['numerator'])
& (~(dogrates_lite['rating_denominator'] != 10)))
]
# 此块目的:在breeds_lite数据集中筛选出上述异常值对应的行,组成数据集breeds_for_wrong_numerator备用;是大块中第二基础的部分
# 以便后续根据该数据集中的p1_dog列判断原tweets_lite数据集中的数据是否有效
breeds_for_wrong_numerator = breeds_lite[breeds_lite['tweet_id'].isin(
tweets_wrong_numerator['tweet_id'])]
# 此块目的:大块的执行
# 利用breeds_for_wrong_numerator,筛选出分子异常数据中为狗的部分
dog_id_filter = breeds_for_wrong_numerator[breeds_for_wrong_numerator['p1_dog']
== True]
# 筛选出分子异常的数据中,大概率数据有效(是狗)的数据
tweets_wrong_numerator_dog = ISIN(
tweets_wrong_numerator, dog_id_filter, 'tweet_id')
tweets_wrong_numerator_dog
上述数据集中的主要问题是分数有小数导致的。在源数据被正则表达式提取的时候,没有考虑过分数可能含有小数的情况,导致程序只匹配了小数点后,分号前的数字作为分子。
假设上述假设正确,是否有可能还有分子存在小数的状况,而我们并没有察觉到呢?当然有。保险起见,我们再查一道:
# 观察后,使用正则表达式匹配所有分子含有小数点的text数据
pattern_score_decimal0 = r'(\d+\.\d+)\/(\d+)'
test_decimal0 = dogrates_lite['text'].str.findall(pattern_score_decimal0)
# 筛选出index标签,以便后续利用
index_score_has_decimal = test_decimal0[test_decimal0.str.len(
) != 0].index.tolist()
# 将两边index个数相减,查看是否一边所含元素个数更多
len(index_score_has_decimal) - len(tweets_wrong_numerator_dog.index.tolist())
看来使用新的方法筛查出了我们之前没有检查出来的数据。我们将根据新的索引筛选出的数据集赋值给tweets_wrong_numerator_dog
# 重新赋值tweets_wrong_numerator_dog数据集
tweets_wrong_numerator_dog = dogrates_lite.loc[index_score_has_decimal]
tweets_wrong_numerator_dog
# 此块目的:大块的执行
# 利用breeds_for_wrong_numerator,筛选出分子异常数据中不为狗的部分
# 为了避免代码又臭又长,分两行写
NOT_dog_id_filter = breeds_for_wrong_numerator[breeds_for_wrong_numerator['p1_dog']
== False]
# 筛选出分子异常的数据中,大概率数据有效(是狗)的数据
tweets_wrong_numerator_NOT_dog = ISIN(
tweets_wrong_numerator, NOT_dog_id_filter, 'tweet_id')
tweets_wrong_numerator_NOT_dog
# 在包含图片的breeds数据集中筛选出对应行,以便展示图象
pic_show = ISIN(breeds_lite, tweets_wrong_numerator_NOT_dog, 'tweet_id')
pic_show_ids = pic_show
pic_show
# show pictures
# 这行代码不报错需要魔法上网
# OK,这张图虽然是狗,但是1776明显是美国独立日啊~drop了drop了,惹不起惹不起~
url1 = "https://pbs.twimg.com/media/CmgBZ7kWcAAlzFD.jpg"
img1 = Image.open(requests.get(url1, stream=True).raw)
img1
# show pictures
# 额这什么鬼~🤣
# 这行代码不报错需要魔法上网
url2 = "https://pbs.twimg.com/media/CU9P717W4AAOlKx.jpg"
img2 = Image.open(requests.get(url2, stream=True).raw)
img2
timestamp列数据类型错误¶dogrates_lite.head(1)
dogrates_lite.info()
与此同时,观察可知,标定是否为转发推特的字段就是retweeted_status_id。当这一字段不为空时,对应的推特为转发。
# retweeted_status_id不为空的转发推特。text列都以“RT @”开头
retweeted_id_notnull.head(2)
# 原创的推特条目
dogrates_lite[dogrates_lite['retweeted_status_id'].isnull()].head(2)
转发的推特有没有可能包含了主数据没有的信息呢?比如转发量?有可能,因此我们还进行如下测试:
breeds_lite和retweets_lite数据集的对应数据。# 获取一条转发的信息和一条主信息
# 就以上面出现的,35行的推特为例
# 至于为什么选35而不选19嘛~我才不告诉你19行对应的转发数据在retweet_lite数据集里不存在呢~
RT_info_test = dogrates_lite[dogrates_lite['text'].str.contains('This is Lilly. She just parallel barked. Kindly requests a reward now.', regex=False)]
RT_info_test
# 找出对应的id对应的breeds_lite数据集信息
ISIN(breeds_lite, RT_info_test, 'tweet_id')
# 找出对应的id对应的retweet_lite数据集信息
retweets_lite[(retweets_lite['id'].isin(RT_info_test['tweet_id']))]
我们发现,转发的推特对应的tweet_id,在breeds_lite数据集中与主信息完全一致;在retweets_lite数据集中,不仅转发推特favorite_count为0,主信息还完全继承了所有转发信息的retweet_count。
因此,我们得出结论,所有转发的推特信息都可以直接清除,而无需担心去除可能会造成信息的缺失。
dogrates_lite数据集
# 计算分类数量的方法参考了以下链接:
# https://blog.csdn.net/u010606346/article/details/84778363
df1 = dogrates_lite.copy()
df1.replace(to_replace='None', value=np.nan, inplace=True, method=None)
# dgr2 = dogrates_lite.copy()
df1['stage_count'] = df1[['doggo', 'floofer',
'pupper', 'puppo']].notnull().sum(axis=1)
_2stages = df1[df1['stage_count'] > 1].sort_values(
by='stage_count', ascending=False)
# 观察发现,所有有两条狗且分类不同的狗狗,都包含有both这个单词,但是有的大写有的小写
_2dogs_1tweet = _2stages[(_2stages['text'].str.contains(
'both')) | (_2stages['text'].str.contains('Both'))]
# _2stages数据集中剩下的数据,就是需要手工清理的,一只狗两个分类的数据(well,起码没有证据证明它们不是一只狗)
_1dog_2stage = ISIN(_2stages, _2dogs_1tweet, 'tweet_id', mismatch=True)
_1dog_2stage
dogrates_lite数据集
这个部分中,一列中确实包含了两只狗狗的信息;这是因为两只狗同框且分数一致,但两种狗狗的分类不同导致的;
# observe
_2dogs_1tweet
其实dogrates_lite数据集里,还有很多条推特信息给多只狗狗同时打了分,下面的代码块将展示这个部分。
但是,与上述代码不同,其基本遵照了多只狗狗相同分数,且都属于同一生长阶段。确实有可能影响最后的分析。但考虑到影响主要体现在狗狗的名字和生长阶段,因此我们不做特殊处理。
# 含有多只狗狗的推特条目示例
df2 = dogrates_lite[dogrates_lite['text'].str.contains('&')]
df2.head()
retweets_lite数据集
id,代表其他数据集中tewwt_id列数据的列,应更名为tweet_id¶retweets_lite.head(1)
接2.2.1.2。
在上述章节中,我们讨论了分子信息被错误提取,导致分母和分子信息被错误提取的问题。当时,我们发现,这一问题是由于推文中存在两个含有“/”符号和周围数字组成的组合,而上一个处理这一数据的人没有留意到这一点。因此,她/他的程序返回了第一个正则表达式匹配到的数值,而很不幸的,在这些推文中,正确的分数往往都是第二组数字。
在整理出了包含上述错误信息的数据集后,我们提出了进一步的疑问:有没有可能数据集中还有其他推文,也包含两个数字,但因为两个数字都是正确的分数,导致我们没能筛查到呢?如果这种情况真的存在,那也就意味着在我们的分析当中,有一部分狗狗的分数没有被体现。这显然不怎么公平。为了更好的探索这一数据集,也为了保证这一探索的严谨性,我们进行如下探索:
# 匹配推文中含有两个数字
pattern_score_multinumbers0 = r'([\d]+\/\d+)\D+([\d]+\/[\d]+)'
multiple_number1 = dogrates_lite['text'].str.findall(
pattern_score_multinumbers0)
# 取两数据集差集
index_multiple_number_case2 = list(set(multiple_number1[multiple_number1.str.len(
) != 0].index.tolist()) ^ set(tweets_multiple_number.index.tolist()))
# 不包含tweets_multiple_number数据集内容,但又被正则表达式匹配出的,其他含有两组数字的数据
tweets_multiple_number_case2 = dogrates_lite.loc[index_multiple_number_case2]
tweets_multiple_number_case2
唔看来问题不算少。就此,我们提出最后一个数据质量问题:
tweets_multiple_number_case2
传统上,整洁度的标准依照R语言大神Hadley Wickham在其划时代的论文Tidy Data中指出的那样,主要由三个标准组成:
使用这样的方式整理数据,使得后续的程序编写和分析都轻松愉快。依照这样的标准,我们发现了如下问题:
dogrates_lite数据集
dogrates_lite.head(1)
综合
retweet_lite数据集,毫无疑问,应该与dogrates_lite数据集合并。这两个数据集观察的对象都是一条条的推特; breeds_lite与dogrates_lite数据集本质上观察对象一致。图像终究是为推文服务,也是推特的一部分。我们注意到,推文中大量使用了第三人称代词,且两种性别都有。下面展示了数据集的前两行,其中就包括了两种性别的狗狗。
推文依赖的第三人称代词表示性别,也就意味着从推文中推断出狗狗的性别是有可能的,而这一属性的加入可能能为我们带来新的思路。尽管进行绝育的狗狗可能不会有明显的性别差异(加入绝大多数推文中的狗狗都已经绝育),不同性别的狗狗同样可能有行为上的差异。
我们将尝试提取每条推文中对应狗狗的性别信息。探索我们可能会发现什么。
dogrates_lite.head(2)
截至目前,我们已经发现了如下问题:
质量
dogrates_lite数据集中,name列有大量空值,和错误的情况;dogrates_lite数据集中,部分数据错误的提取了其他包含“/”的文本作为分数,这些数据被保存在了tweets_mulpitple_number数据集中;dogrates_lite数据集中,有一行数据虽然包含数字,但是一条筹款的推文,并不包含评分;这条数据被保存在了tweets_fund_raise数据集中;dogrates_lite数据集中,部分数据存在多只狗狗统一打(总)分的情况,导致这些条目的分子和分母显著较高;这些数据储存在tweets_multiple_dogs中;dogrates_lite数据集中,部分数据存在推文主题真的是狗狗的情况下,有分子提取错误的情况;主要体现在这些分数为了某些纪念日等,使用了特殊的小数分数;这些数据储存在tweets_wrong_numerator_dog中;dogrates_lite数据集中,部分数据在主题可能不是狗的情况下,有分子提取错误的情况;这些数据储存在了tweets_wrong numerator_NOT_dog数据集中;dogrates_lite数据集中,timestamp列数据类型错误;dogrates_lite数据集中,有部分数据属于转发的推特,与原始数据重复;dogrates_lite数据集中,部分数据里只有一只狗,却因为text列中包含两个狗狗分类的信息,而拥有两个分类;这些数据被储存在了_1dog_2stage数据集中;dogrates_lite数据集中,部分数据,一条推特对两只处于不同生长阶段的打了同样的分数,因此拥有两个分类;这些数据被储存在了_2dogs_1tweet数据集中;retweets_lite数据集中,id列应更名为tweets_id,与dogrates_lite和breeds_lite保持一致;dogrates_lite数据集中,有部分数据包含两组正确的分数,但只提取了一组;这些数据被保存在了tweets_multiple_number_case2数据集中(tweets_multiple_number中的数据不在此列)。整洁度
dogrates_lite数据集中,doggo``floofer``pupper``puppo四列是一个变量的观察结果,应该被储存在一列中;- 包含转发和点赞信息的
retweets_lite数据集和dogrates_lite数据集应当合并,因其观察的而对象是相同的。其他
- 提取推文中狗狗的性别因素
# 准备数据集
dogrates_clean = dogrates_lite.copy()
breeds_clean = breeds_lite.copy()
retweets_clean = retweets_lite.copy()
要对已经发现的数据问题进行清洗和整理,我们首先需要备份我们已有的数据集(见上)。我们统一为干净的数据集名称加上_clean的尾缀。
因为数据集的问题委实不少,我们确定如下清理顺序:
因为数据集情况比较复杂,我们决定,在此项报告中,所有清洗出的数据都必须遵照一条tweet_id对应一行的原则。原因如下:
.melt函数后,我们将面临大量的重复数据,且我们没有很好的办法筛选出准确的数据。这一原则也同样意味着:
# observe
tweets_fund_raise
define: 使用drop方法, 删除行标签为516的行
# code
dogrates_clean.drop([516], axis=0, inplace=True)
# test
dogrates_clean[dogrates_clean.index == 516]
Test OK!
# observe
tweets_wrong_numerator_NOT_dog
# code
dogrates_clean.drop([979, 2074], axis=0, inplace=True)
# test
ISIN(dogrates_clean, tweets_wrong_numerator_NOT_dog, 'tweet_id')
Test OK!
define: 正如我们在2.2.1.2章节第7小节中叙述的那样,直接清除转发推特数据不会造成任何影响;
鉴于我们之前已经根据retweeted_status_id这一字段筛选出了转发的推特数据,并确认了这一字段时一条推特是否为转发推特判断的充分必要条件,我们可以根据这一数据集清除转发推特数据,并进行测试。
# code
dogrates_clean = ISIN(dogrates_clean, retweeted_id_notnull,
'tweet_id', mismatch=True)
dogrates_clean.drop(columns='retweeted_status_id', inplace=True)
# test
pattern_RT = r'(^RT\s\@\w+:\s)'
tweets_RT = dogrates_clean['text'].str.findall(pattern_RT)
tweets_RT[tweets_RT.str.len() != 0]
dogrates_clean.head(2)
Test OK! 成功去掉了转发的推特数据!😎
name列有大量空值和错误的情况¶define: 这个部分中,我们将重新提取名字信息,并想办法好看的合并多个名字。细节如下:
name2列之中;重新提取的名字信息将保存在dog_names_reworked数据集中;name2列中的值是否为空,判断图片中的狗狗是否只有一只;这一值会被保存在singe_dog列中;dogrates_clean数据集中的同名列;这其中包括:name2中的NaN值,以为后续合并做准备;name1和name2列;single_dog列为False时,name1和name2将被" & "链接;name列都将与name1列的值保持一致。dog_names_reworked和dogrates_clean两个数据集,并将两个数据集合并。观察推文中的名字:
推文中出现狗狗名字的推文规律一览:
- These are Peruvian Feldspars. Their names are Cupit and Prencer. Both resemble Rand Paul. Sick outfits 10/10 & 10/10 https://t.co/ZnEMHBsAs1;
- This is Ben & Carson. It's impossible for them to tilt their heads in the same direction. Cheeky wink by Ben. 11/10s https://t.co/465sIBdvzU;
- This is Pipsy. He is a fluffball. Enjoys traveling the sea & getting tangled in leash. 12/10 I would kill for Pipsy https://t.co/h9R0EwKd9X;
- These two dogs are Bo & Smittens. Smittens is trying out a new deodorant and wanted Bo to smell it. 10/10 true pals https://t.co/4pw1QQ6udh;
- Say hello to Bobb. Bobb is a Golden High Fescue & a proud father of 8. Bobb sleeps while the little pups play. 11/10 https://t.co/OmxouCZ8IY;
- Meet Jaycob. He got scared of the vacuum. Hide & seek champ. Almost better than Kony. Solid shampoo selection. 10/10 https://t.co/952hUV6RiK;
- Meet Jeb & Bush. Jeb is somehow stuck in that fence and Bush won't stop whispering sweet nothings in his ear. 9/10s https://t.co/NRNExUy9Hm;
- Here we have Pancho and Peaches. Pancho is a Condoleezza Gryffindor, and Peaches is just an asshole. 10/10 & 7/10 https://t.co/Lh1BsJrWPp;
我们提取狗狗名字时应当利用的规律总结如下:
所有的名字出现之时,都跟随着如下句式(名字的位置用name/names代替):
- This is
name(s)/These arenames;- These two dogs are
names;- Say hello to
name;- Meet name/Meet
names;- Here we have
names;
# code
# 重新提取名字;考虑到有不少两只狗的情况,提取到的第二个名字单独保存为一列;
pattern_multiple_names = r'(?:Say hello to|This is|These two dogs are|Meet|Here we have|Their names are)\s(?P<name1>[A-Z][a-z]*)\s?(?:and|&)?\s?(?P<name2>[A-Z][a-z]*)?(?:\s[A-Z][a-z]*\.)?'
dog_names_reworked = dogrates_clean['text'].str.extract(pattern_multiple_names)
# 新增一列SingleDog,用于确定推文中是否只有一只狗。若为True,则只有一只狗;若False,则可能有多只
dog_names_reworked['single_dog'] = dog_names_reworked['name2'].isna()
# 为了简洁性,准备合并name1和name2列
# 合并之前,需要清理name2列中的NaN数据
dog_names_reworked['name2'].fillna(value='', inplace=True)
# 为了更好的体验,我们加入一列作为两个名字之间的连接符;将最终的结果输出为name列后,去掉不相干的列
# https://stackoverflow.com/questions/19913659/pandas-conditional-creation-of-a-series-dataframe-column
# 这个地方应该有更好的办法
dog_names_reworked['connector'] = np.where(
dog_names_reworked['single_dog'] == False, " & ", "")
dog_names_reworked['name'] = dog_names_reworked['name1'] + \
dog_names_reworked['connector'] + dog_names_reworked['name2']
dog_names_reworked.drop(columns=['name1', 'name2', 'connector'], inplace=True)
# drop掉dogrates_clean数据集原有的name列,然后将两数据集合并
dogrates_clean.drop(columns='name', inplace=True)
dogrates_clean = dogrates_clean.join(dog_names_reworked)
# test-1
dog_names_reworked.loc[[1, 461, 1366]]
# test-2
dogrates_clean.loc[[1, 461, 1366]]
# test-3
dogrates_clean['name'].value_counts()
Test OK!😎 没有什么乱七八糟的名字了!两只在一起的狗狗也真的在一起了!
# observe
_1dog_2stage
在这个错误中,推主自己在推文中列举了几个容易混淆的狗狗类型,导致这些狗狗实际上拥有多个分类。
好吧~没什么特别好的办法,手工改一下🤢。手工更改的方法参考了这篇文章(output 42)。
问题主要集中在doggo和pupper两列:
# code-1
dic_doggo = {191: np.nan, 200: np.nan, 575: np.nan, 956: np.nan}
for (key, value) in dic_doggo.items():
dogrates_clean.loc[key, 'doggo'] = value
# code-2
dic_pupper = {460: np.nan, 705: np.nan, 956: np.nan}
for (key, value) in dic_pupper.items():
dogrates_clean.loc[key, 'pupper'] = value
# test
dogrates_clean.loc[_1dog_2stage.index.tolist()]
Test OK!😎 没有什么奇怪的一狗两态了!
# observe
_2dogs_1tweet
看来全部都是大狗(Doggo)带小狗(Pupper)啊! ^0^
define: 这个部分中的,根据既定原则,我们将想办法合并有多个生长状态分类的推特。细节如下:
None替换为Nan。尽管_2dogs_1tweet中的分类空值都已被转换为NaN,但在dogrates_clean数据集中我们还没有进行相应的转化;Not Specified,以更好的认知这一分类的影响。# code
# 首先将'None'处理为NaN,方便计数
dogrates_clean.replace(to_replace='None', value=np.nan,
inplace=True, method=None)
# 计算狗狗的分类数量
dogrates_clean['stage_count'] = dogrates_clean[['doggo', 'floofer',
'pupper', 'puppo']].notnull().sum(axis=1)
# 为了更好的体验,我们加入一个临时列作为两个名字之间的连接符;将最终的结果输出为name列后,去掉不相干的列
# https://stackoverflow.com/questions/19913659/pandas-conditional-creation-of-a-series-dataframe-column
# 这个地方应该有更好的办法
dogrates_clean['connector'] = np.where(
dogrates_clean['stage_count'] >= 2, " & ", "")
# 再把空值处理掉,为合并列做准备
stages = ['doggo', 'floofer', 'pupper', 'puppo']
for x in stages:
dogrates_clean[x].fillna(value='', inplace=True)
# 处理文字贴合的部分
dogrates_clean['stage(s)'] = dogrates_clean['doggo'] + dogrates_clean['floofer'] + \
dogrates_clean['connector'] + \
dogrates_clean['pupper'] + dogrates_clean['puppo']
# 去掉不(再)需要的列
dogrates_clean.drop(columns=['doggo', 'floofer',
'pupper', 'puppo', 'connector'], inplace=True)
# 再把NaN改回来 U_U
dogrates_clean['stage(s)'].replace('', 'Not Specified', inplace=True)
# test-1
ISIN(dogrates_clean, _2dogs_1tweet, 'tweet_id')
# test-2
dogrates_clean['stage(s)'].value_counts()
# test-3
dogrates_clean.head()
Test OK!😎 大狗狗和小狗狗从此幸福快乐的生活在了一起!
# observe
tweets_multiple_number
define:
我们观察到,这些行的分数之所以被错误的提取,是因为:
text列中包含了两个含有“/”,两边包含数字的组合为了解决这一问题,我们需要进行如下操作:
dogrates_clean数据集的text列,使用正则表达式重新提取分数;correct_scores数据集;数据集中。# code
# 针对上述行重新提取分数
pattern_score_multinumbers = r'(?:[\d]+\/\d+)\D+(?P<rating_numerator>[\d]+)\/(?P<rating_denominator>[\d]+)'
correct_scores = dogrates_clean['text'].str.extract(
pattern_score_multinumbers, expand=True)
# 只保留需要更改的行,构成新的数据集
correct_scores_sliced = correct_scores.loc[ix_wrong_rates]
# 通过update将量数据集合并;两个数据集将自动根据index和column对齐
dogrates_clean.update(correct_scores_sliced)
# test
dogrates_clean.loc[ix_wrong_rates]
Test OK!😎 没有什么因为两个斜杠(“/”)导致的错误的分数了!
# observe
tweets_wrong_numerator_dog
# code
# 针对上述行重新提取分数
pattern_score_decimal = r'(?:(?P<rating_numerator>\d+\.\d+)\/(?:\d+))'
correct_scores_decimal = dogrates_clean['text'].str.extract(
pattern_score_decimal, expand=True)
# 通过保存错误分数的数据集获取用于切片的索引标签
slicing_index_decimal = tweets_wrong_numerator_dog.index.tolist()
# 只保留需要更改的行,构成新的数据集
correct_scores_decimal_sliced = correct_scores_decimal.loc[slicing_index_decimal]
# 通过update将量数据集合并;两个数据集将自动根据index和column对齐
dogrates_clean.update(correct_scores_decimal_sliced)
# test-1
dogrates_clean.loc[slicing_index_decimal]
# test-2
pattern_score_decimal1 = r'((?P<rating_numerator>\d+\.\d+)\/(\d+))'
test5 = dogrates_clean['text'].str.findall(pattern_score_decimal1)
test5[test5.str.len() != 0]
Test OK!两个test的结果也符合😎。没有什么错误的分数了!
# observe
tweets_multiple_number_case2.head()
define:
我们观察到,这些行的分数之所以没有被提取完全,是因为:
text列中包含了两个含有“/”,两边包含数字的组合;为了解决这一问题,我们需要进行如下操作:
dogrates_clean数据集的text列,使用正则表达式重新提取分数;missing_2nd_score数据集;# code-1: Find
# 正则匹配规律
pattern_score_multinumbers1 = r'(?:[\d]+\/\d+)\D+(?P<rating_numerator2>[\d]+)\/(?P<rating_denominator2>[\d]+)'
# 利用正则表达式提取文本信息
missing_2nd_score = dogrates_clean['text'].str.extract(
pattern_score_multinumbers1)
# 将两个数据集合并
dogrates_clean = dogrates_clean.join(missing_2nd_score)
# test-1
ISIN(dogrates_clean, tweets_multiple_number_case2, 'tweet_id')
# test-2
# pattern_score_multinumbers2 = r'([\d]+\/[\d]+)\D+([\d]+\/[\d]+)'
test_tweet_multiple_scores = dogrates_clean['text'].str.findall(pattern_score_multinumbers1)
test_tweet_multiple_scores[test_tweet_multiple_scores.str.len() != 0]
上述的测试中,出现了索引标签为2335的行,在我们清理的过程中没有出现;二者使用的正则表达式是一致的。谨慎起见,我们看一下:
dogrates_clean.loc[[2335]]
我们的算法单独提取了第二个数据,因此之前修复过的,应该也不影响。分数正确,看来2335行也没有问题。
又是一个愉快的Test OK!无论有几个分,我们的数据集都能完美的体现了! :D
# observe
tweets_multiple_dogs
define: 我们将统一分数的标准:以10分治为基础,每条推文对应的分数都将是推文中包含的所有分数的均分。
这样的操作处于以下两点考虑:
细节如下:
NaN值填为0,使其不影响我们的计算;# code
# 为数据加和进行数据类型转换,否则他们都是字符串
numbers = ['rating_denominator2', 'rating_numerator2',
'rating_denominator', 'rating_numerator']
for x in numbers:
dogrates_clean[x].fillna(value=0, inplace=True)
dogrates_clean[x] = dogrates_clean[x].astype(float)
# 计算平均分
dogrates_clean['average'] = ((dogrates_clean['rating_numerator'] + dogrates_clean['rating_numerator2']) /
(dogrates_clean['rating_denominator'] + dogrates_clean['rating_denominator2'])) * 10
# 去掉不(再)需要的列
dogrates_clean.drop(columns=['rating_numerator', 'rating_denominator',
'rating_numerator2', 'rating_denominator2'], inplace=True)
# test-1
ISIN(dogrates_clean, tweets_multiple_dogs, 'tweet_id')
本小节处理的问题看起来没有什么问题;我们再看看其他处理数据小节的结果是否受到影响。
# test
ISIN(dogrates_clean, tweets_multiple_number_case2, 'tweet_id').head()
上一小节的处理看起来也没有什么问题。
那我们只好愉快的得出Test OK的结论啦!😁
timestamp列数据类型问题¶define: 使用pandas的to_datetime方法将timestamp列转变成datatime数据
# code
dogrates_clean['timestamp'] = pd.to_datetime(dogrates_clean['timestamp'])
# test-1
dogrates_clean.info()
# test-2
dogrates_clean.head(1)
Test OK! 😎
retweets_lite数据集id列为tweet_id,与dogrates_lite和breeds_lite保持一致。¶define 使用rename方法更改数据集列名称,并检验
# code
retweets_clean.rename(columns={'id': 'tweet_id'}, inplace=True)
# test
retweets_clean.columns
breeds_clean数据集中的名称¶define: 将breeds_clean数据集中所有包含'p1'的列名称全部改为breed
# code
breeds_clean.rename(columns={'p1': 'breed'}, inplace=True)
# test-1
breeds_clean.head(2)
# test-2
breeds_clean.info()
doggo, floofer, pupper, puppo四列是一个变量的观察结果,应该被储存在一列中;¶# observe
dogrates_clean.columns
已解决👌
retweets_lite数据集和dogrates_lite数据集应当合并,因其观察的而对象是相同的。¶# code
dogrates_clean = dogrates_clean.merge(retweets_clean, on='tweet_id')
# test-1
dogrates_clean.head()
# test-2
dogrates_clean.info()
breeds_lite数据集和dogrates_lite数据集应当合并,因其观察的而对象是相同的。¶# code
dogrates_clean = dogrates_clean.merge(breeds_clean, on='tweet_id')
# test-1
dogrates_clean.head()
# test-2
dogrates_clean.info()
text列中提取性别信息¶define:
提取性别信息有多种方法。Anouar ZBAIDA和Merzu K Belete在他们的项目里使用的方法是历遍所有的行,查看是否含有他指定的人称代词列表内的单词,并通过函数赋值。但是,他们都没有使用正则,且他们忽视了大小写这一重要因素,导致他们的性别数据不准。在详细(又忐忑)的对比了输出之后,我们发现他们的算法忽视了许多在句首的He和His,导致公狗狗的统计数据显著缺失。为了避免这种人为错误,我们需要使用正则表达式来尽可能精准的匹配每条推文。
具体来说,我们将进行如下操作:
# 通过男性人称代词确定公狗狗,得到一列包含布尔值的Series;将其重命名为Male
pattern_male = r"(?:\W)([h|H](?:e(?:\s|'s)|im(?:self)?|is))"
male_status = dogrates_clean['text'].str.contains(pattern_male)
# 将其重命名为Female,否则与下面的female_status合并后,得到的dataframe两列都叫text;melt之后就更难看了;
male_status = male_status.rename('Male')
# To下面的警告:知道,谢谢~
# 通过女性人称代词确定母狗狗,得到一列包含布尔值的Series;
pattern_female = r"(?:\W)([s|S]he(?:\s|'s)|[H|h]er(?:\s|self))"
female_status = dogrates_clean['text'].str.contains(pattern_female)
# 将其重命名为Female,否则与上面的male_status合并后,得到的dataframe两列都叫text;melt之后就更难看了;
female_status = female_status.rename('Female')
# To下面的警告:知道,谢谢~
# 合并两个series成dataframe,命名为dog_gender_bool
dog_gender_bool = pd.concat([male_status, female_status], axis=1)
dog_gender_bool.head(5)
显然,数据集里不是只有Male和Female两种:
Unkown;Tnvestigate。为此,我们依旧参照3.1.2中对名字数量的判断生成对应值的办法,列出对应条件生成对应的值。方法依旧使用np.where,依旧是参考的这篇文章。这一部分整理完全之后,每一行数据都有了对应的值,使得后续使用melt生成最终的gender列成为可能。
# 新建列Investigate:当Male和Female都为真时,值为真;数据集生成后需要进一步调差
dog_gender_bool['Investigate'] = np.where((dog_gender_bool['Male'] == True) & (dog_gender_bool['Female'] == True), True, False)
# 新建列Unknwon:当Male和Female都为假时,值为真
dog_gender_bool['Unknown'] = np.where((dog_gender_bool['Male'] == False) & (dog_gender_bool['Female'] == False), True, False)
# 还有特别重要的一步!把标定为Investigate的行,Male和Female都改为False,不然,这些列会重复三次!
dog_gender_bool['Male'].where(cond=(dog_gender_bool['Investigate'] != True), other=False, inplace=True)
dog_gender_bool['Female'].where(cond=(dog_gender_bool['Investigate'] != True), other=False, inplace=True)
# 为方便下一步执行melt和之后的一系列合并操作,重设dog_gender_bool数据集的索引
dog_gender_bool = dog_gender_bool.reset_index()
# 打印几行有代表性的出来看一眼;四列中只能由一个为真
# 想知道这四个数怎么来的?Debug的时候发现的
dog_gender_bool.loc[[0, 1, 1084, 1990]]
# 使用melt方法,将四列性别信息转化为一列
dog_gender = dog_gender_bool.melt(id_vars=['index'], value_vars=[
'Male', 'Female', 'Unknown', 'Investigate'], var_name='gender')
# 使用melt方法之后,同一行会变成四行,并分别对应dog_gender_bool中,表明性别的四列的值
dog_gender[dog_gender['index'] == 0]
# 使用melt方法之后,同一行会变成四行;
# 而在我们前期准备充分,每一行都有对应的gender信息;因此我们只需保留value列为True的行就好
dog_gender = dog_gender[dog_gender['value'] == True]
# 整理一下,去掉value列,按照index排个序,再把提取出来的index列重设为数据集的索引,方便后续合并数据集
dog_gender = dog_gender.sort_values('index').drop(columns='value')
dog_gender.set_index('index', drop=True, inplace=True)
# 检查值的分布情况
dog_gender['gender'].value_counts()
# 将性别信息合并到主数据及dogrates_clean中
dogrates_clean = dogrates_clean.join(dog_gender)
看起来性别信息都正确的匹配了。
我们看到,有5个被标记为需要调查的性别信息。我们接下来看一下需要调查的,既是male又是female的狗狗们。
# code-2_observe
gender_investigate = dogrates_clean[dogrates_clean['gender'] == 'Investigate']
gender_investigate
这些推文出现识别错误的原因在于有人类男性/女性乱入。565行甚至就是人类。没什么特别好的办法,手动修改一下
# code-2_code
# 565行后续处理品类时统一处理
# 修改错误的性别信息
dict_gender = {268: 'Male', 272: 'Male', 1084: 'Male', 1706: 'Female', 565: 'Female'}
for (key, value) in dict_gender.items():
dogrates_clean.loc[key, 'gender'] = value
# code-2_test
ISIN(dogrates_clean, gender_investigate, 'tweet_id')
# test-1
dogrates_clean.sample(5)
RT/Like¶# code
dogrates_clean['RT/Like'] = dogrates_clean['retweet_count'] / \
dogrates_clean['favorite_count']
# test
dogrates_clean.head(1)
define:哪些推文,什么时候,在点赞量和转发率上创下新高呢?把握这些关键节点,显然可以帮助我们梳理出推特帐号发展历程中的重要推特
df333 = dogrates_clean.sort_values(by='timestamp')
df333['cummax_like'] = df333['favorite_count'].cummax()
df333['cummax_RT/Like'] = df333['RT/Like'].cummax()
df333.head(1)
dogrates_milestones_favorite = df333.drop_duplicates(
subset=['cummax_like'], keep='first')
dogrates_milestones_RTRate = df333.drop_duplicates(
subset=['cummax_RT/Like'], keep='first')
dogrates_milestones_favorite
dogrates_milestones_RTRate
OK! 我们现在知道了几个点赞量和转赞比的里程碑。嗯,其中好像没有大火的,关于Brant的两条。
# observe
dogrates_clean['average'].describe()
define:有没有可能,低评分和高评分,实际上是几种不同特征的表现呢?我们将把分数分为4组,探讨极低,低,一般和高分分组之间,在内容和受欢迎程度上的区别。
考虑到分数的最小值为0,最大值为14,而至少75%以上的分数,我们不按照分数分布的占比来区分。我们定义如下绝对的分数区间(前闭后开),作为分组依据:
| 分数段 | 类别(中) | 类别(英) |
|---|---|---|
| 0-3.5 | 极低 | Very Low |
| 3.5-7 | 低 | Low |
| 7-10 | 中 | Medium |
| 10-14 | 高 | High |
# code
average_cat_labels = ['very_low', 'low', 'medium', 'high']
dogrates_clean['average_cate'] = pd.cut(dogrates_clean['average'],
bins=[-1, 3.5, 7, 10, 15],
right=True, labels=average_cat_labels)
# test
dogrates_clean['average_cate'].value_counts()
OK! 评分分组完成!
define:仔细观察数据集后(嗯,也就大迭代8次,翻了100多条推文把,很快的😏),我们发现,机器学习图像的结果,可以将推文大体可以分为三类:
这三者之间显然代表了完全不同的特性:
这三种内容都是推特账号的有机组成部分,因此我们绝不会删除它们。但是,上述分析所蕴含的意义是,这三种推文内容的受众群体和受喜爱程度可能并不一致,因此非常值得进一步探索。
为了更好的帮助大家理解我们在说什么,我们举三个例子😋。
# 为了更方便的展示数据集中的图片,我们定义如下函数
def show_tweet_pic(column=None, method='max', tweet_id=None):
'''
我们定义show_tweet_pic函数,用于在数据集合并后更好的展示图片(仅支持合并后的主数据集);
函数接收三个变量:列名column,方法method(max , min, tweet_id三选一,默认max),和tweet_id;
当方法选择为min或max时,列名必填;函数展示指定列最大或最小值的图片和推文内容;
当方法选择为tweet_id时,tweet_id必填;函数展示指定tweet_id的图片和推文内容;
永远不要输错,不然后果自负 ^_~
'''
if method == 'max':
row = dogrates_clean.loc[[dogrates_clean[column].idxmax()]]
elif method == 'min':
row = dogrates_clean.loc[[dogrates_clean[column].idxmin()]]
elif method == 'tweet_id':
row = dogrates_clean[dogrates_clean['tweet_id'] == int(tweet_id)]
else:
print('Invalid input has triggered safty protocols. This computer self detonate within 5 seconds... ^_+')
url = row['jpg_url'].to_string(index=False)
img = Image.open(requests.get(url, stream=True).raw)
display(img)
display(row['text'])
# example1: Definitely Dog
show_tweet_pic(method='tweet_id', tweet_id = 666649482315059201)
example_definitelydog = dogrates_clean.query(
"tweet_id == '666649482315059201'")
example_definitelydog[['tweet_id', 'timestamp', 'average',
'text', 'breed', 'p1_dog', 'p2_dog', 'p3_dog', 'RT/Like']]
好一个definitely dog! 🙄🙄🙄🙄🙄🙄🙄🙄🙄🙄🙄🙄🙄🙄🙄🙄🙄🙄🙄🙄🙄🙄🙄🙄
# example 2: Possibly Dog
show_tweet_pic(method='tweet_id', tweet_id = 676219687039057920)
example_possiblydog = dogrates_clean.query("tweet_id == '676219687039057920'")
example_possiblydog[['tweet_id', 'timestamp', 'average',
'text', 'breed', 'p1_dog', 'p2_dog', 'p3_dog', 'RT/Like']]
你看我说的没错吧~狗狗和其他什么的在一起~
# example 3: Not Dog
show_tweet_pic(method='tweet_id', tweet_id = 675153376133427200)
example_notdog =dogrates_clean.query("tweet_id == '675153376133427200'")
example_notdog[['tweet_id', 'timestamp', 'average', 'text',
'breed', 'p1_dog', 'p2_dog', 'p3_dog', 'RT/Like']]
嗯,完美。这张图上确实只有桌子和白地毯,这次机器学习终于不犯傻了~
Anyway, you get the idea.😀
机器学习的判断结果虽然会出现一些极端情况,但的确,综合其预测的三个品种,我们发现其可以很好的区分推文内容,且这些内容之间可能有本质的区别。因此,我们根据机器学习模型预测的结果对其进行分组。
# code - count "True"s
dogrates_clean['p1_dog'] = dogrates_clean['p1_dog'].map({True: 1, False: 0})
dogrates_clean['p2_dog'] = dogrates_clean['p2_dog'].map({True: 1, False: 0})
dogrates_clean['p3_dog'] = dogrates_clean['p3_dog'].map({True: 1, False: 0})
dogrates_clean['isdog_index'] = dogrates_clean['p1_dog'] + dogrates_clean['p2_dog'] + dogrates_clean['p3_dog']
# test - count "True"s
dogrates_clean['isdog_index'].value_counts()
# code categorize
is_dog_labels = ['not dog', 'possibly dog', 'definitly dog']
dogrates_clean['dog?'] = pd.cut(dogrates_clean['isdog_index'],
bins=[-1, 0, 2, 3],
right=True, labels=is_dog_labels)
dogrates_clean = dogrates_clean.drop(
columns=['p1_dog', 'p2_dog', 'p3_dog', 'isdog_index'])
# test1
dogrates_clean['dog?'].value_counts()
# test2
dogrates_clean.head(1)
Test OK!我们高兴的宣布:本报告的数据清洗部分(终于)全部完成了!🤩
dogrates_clean.to_csv('twitter_archive_master.csv', na_rep='NaN', header=True)
dogrates_clean.info()
dogrates_clean.nunique()
dogrates_clean.describe()
# 获取每个分类的计数情况:
def value_count(inputs, labels=None, explode=(0.1, 0.1), data=dogrates_clean):
'''此函数用于快速创建饼状图,以查看某一列中值的分布情况;
函数必须传入“列名inputs”变量,“标签labels”和“爆炸explode”可选;
- “列名inputs”将自动作为图片标题的一部分,其首字母会自动大写;
- 当“标签”传入时,会以逆时针方向为饼状图的切片插入用户自定义的标签;
- 当“爆炸”传入时,可以时饼状图的各个部分分开;
这一函数绝大多数情况下适用于分类变量,但在某些非分类变量可以用于分类的情况下也完全适用;
以下新加入的两个功能是针对这一场景的优化;它们能很好的避免庞杂的数字混成一团,保持图片的整洁的同时给予充分的信息:
1) 当标签未指定时,函数会自动为前95%的数据添加上标签;
2) 自动生成的百分比也将仅针对前95%的数据'''
def valid_pct(pct):
'''此子函数用于忽略一切比例小于5%类别的百分比显示;
参考:https://stackoverflow.com/questions/34035427/conditional-removal-of-labels-in-matplotlib-pie-chart'''
return ("%.2f%%" % pct) if pct > 5 else ''
def auto_labling(labels):
'''此子函数的作用是,当labels未指明时,自动生成标签并忽略一切比例小于5%类别的标签显示;当labels指明时,不对labels做处理;
参考:https://stackoverflow.com/questions/34035427/conditional-removal-of-labels-in-matplotlib-pie-chart'''
if labels is not None:
labels = labels
else:
labels_dict = dogrates_clean[str(inputs)].value_counts().to_dict()
labels_auto = [i if n/dogrates_clean['average'].count()
> 0.05 else '' for (i, n) in labels_dict.items()]
labels = labels_auto
return labels
plt.pie(list(data[inputs].value_counts()), labels=auto_labling(labels),
explode=explode, radius=1.2, autopct=valid_pct, startangle=90)
plt.axis('equal') # 为了画出来不是莫名其妙的椭圆
plt.suptitle("Distribution of Dogs on {}".format(str(inputs).capitalize(),
fontweight="bold"))
return dogrates_clean[str(inputs)].value_counts()
# 查看推文中是否有多只狗狗的占比
value_count('single_dog', ['One Dog', 'Many Dogs'])
# 查看狗狗的分类数量占比
value_count('stage_count', ['Not Specified',
'One Stage', 'Two Stages'], explode=(0.1, 0.1, 0.1))
# 对数据进行预处理,去除未指明(Unspecified)的分类情况的数据
df_stage_pie_x = dogrates_clean.copy()
df_stage_pie_x['stage(s)'] = df_stage_pie_x['stage(s)'].replace('Not Specified', np.nan)
fig0, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 6.2), sharex=False)
# ax1 = value_count('stage(s)', explode=(0.1, 0.1, 0.1, 0.1, 0.1, 0.1))
# ax2 = value_count('stage(s)', labels=['Pupper', 'Doggo', 'Puppo', '', ''], explode=(0.1, 0.1, 0.1, 0.1, 0.1), data=df_stage_pie_x)
ax1.pie(list(dogrates_clean['stage(s)'].value_counts()),
labels=['Not_Specified', 'Pupper', 'Doggo', 'Puppo', '', ''],
explode=(0.2, 0.1, 0.1, 0.1, 0.1, 0.1),
radius=1.2, startangle=90,
colors=["#00B8AA", "#083B5B", "#FD615E", "#F1C70E", "#616869", "#51CCC3"])
ax2.pie(list(df_stage_pie_x['stage(s)'].value_counts()),
labels=['Pupper', 'Doggo', 'Puppo', '', ''],
explode=(0.06, 0.03, 0.03, 0.03, 0.03),
radius=0.5, startangle=90,
colors=["#083B5B", "#374649", "#F1C70E", "#616869", "#51CCC3"])
ax1.axis('equal') # 为了画出来不是莫名其妙的椭圆
ax2.axis('equal')
plt.suptitle("Distribution of Dogs on Stage(s)", fontweight="bold")
dogrates_clean['stage(s)'].value_counts()
# 查看狗狗的分数占比
value_count('average_cate', explode=(0.1, 0.1, 0.1, 0.1))
# 创建按照点赞量,转发量,平均转赞比聚合的数据透视表
pivot_breeds_enhanced = pd.pivot_table(dogrates_clean,
values=['tweet_id', 'favorite_count', 'retweet_count', 'average', 'RT/Like'],
index=['breed'],
aggfunc={'favorite_count': [np.mean, np.var], # 单纯的比较总数不太公平,我们也得考虑每条推特的平均点赞量
'retweet_count': np.mean, # 单纯的比较总数不太公平,我们也得考虑每条推特的平均转发量
'RT/Like': [np.mean, np.var],
'tweet_id': len})
# 筛选出绝对是狗的条目,忽略其他内容
filter_is_dog = dogrates_clean[dogrates_clean['dog?'] == 'definitly dog']['breed'].unique().tolist()
pivot_breeds_enhanced = pivot_breeds_enhanced.loc[filter_is_dog]
# 将pivot_breeds_enhanced数据集按照点赞量和转发量排序,备用
pivot_breeds_enhanced = pivot_breeds_enhanced.sort_values(by=[('favorite_count', 'mean'), ('retweet_count', 'mean')], ascending=False)
# 为绘制品种分布情况创建备用数据集,将其按照推文数量排序;只保留前20个
pivot_breeds = pivot_breeds_enhanced.copy()
pivot_breeds = pivot_breeds.sort_values(by=[('tweet_id', 'len')], ascending=False)
pivot_breeds = pivot_breeds.head(20)
# 绘制品种数量分布的柱状图
fig435, ax4350 = plt.subplots(figsize=(9, 12))
fig435.tight_layout()
# 绘制图形,设定颜色
sns.barplot(x=pivot_breeds[('tweet_id', 'len')], y=pivot_breeds.index, ax=ax4350, color='#58C9C0')
# 设置大图标题
plt.suptitle('Distribution of Dogs on Breeds', fontweight='bold', y=1.02)
sns.despine()
唔,金毛和拉布拉多是最多的,毫不意外~😆
value_count('gender', explode=(0.1, 0.1, 0.1))
value_count('dog?', explode=(0.1, 0.1, 0.1))
# 图形初始化
fig441, ax441 = plt.subplots(2, 2, figsize=(10, 10))
# 绘制一般分布情况
sns.distplot(dogrates_clean[('favorite_count')], ax=ax441[0, 0], color="#01B8AA", axlabel='Favorite Count Distribution')
sns.distplot(dogrates_clean[('retweet_count')], ax=ax441[0, 1], color="#374649", axlabel='Retweet Count Distribution')
# 取10的对数进行标准化,并绘制分布情况
sns.distplot(dogrates_clean[('favorite_count')].apply(np.log10), ax=ax441[1, 0], color="#01B8AA", axlabel='Favorite Count Distribution (Normalized)')
sns.distplot(dogrates_clean[('favorite_count')].apply(np.log10), ax=ax441[1, 1], color="#374649", axlabel='Retweet Count Distribution (Normalized)')
sns.despine()
# 设置大图标题
plt.suptitle('Distribution of Favorite & Retweet Counts', fontweight='bold', y=.91)
# 设置风格
sns.set(style='whitegrid')
# 绘制点赞量与转发量的关系图
ax442 = sns.jointplot(x=dogrates_clean['favorite_count'].apply(np.log10), y=dogrates_clean['retweet_count'].apply(np.log10),
kind='reg', color='#01B8AA', height=9)
# 设置轴标题
ax442.set_axis_labels("Favorite Count (Normalized)", "Retweet Count (Normalized)")
# 设置大图标题
plt.suptitle('Favorite & Retweet Distribution (Normalized) & Relation ',
fontweight='bold', y=1.01)
观察:
# 设置风格
sns.set(style='white')
# 图形初始化
fig442, (ax4421, ax4422) = plt.subplots(1, 2, figsize=(10, 5))
# 转赞比分布
sns.distplot(dogrates_clean['RT/Like'], color="#01B8AA",
ax=ax4421, axlabel='Retweet/Favorite Ratio Distribution')
# 取自然常数e的对数
sns.distplot(dogrates_clean['RT/Like'].apply(np.log), color="#01B8AA",
ax=ax4422, axlabel='Retweet/Favorite Ratio Distribution (Normalized)')
# 设置大图标题
plt.suptitle('Distribution of Retweet/Like(Favorite) Ratio',
fontweight='bold', y=.93)
观察:
# 找不到好的图,好看的图画出来的字压根就看不清,就这样吧
# 参考:
# https://amueller.github.io/word_cloud/auto_examples/simple.html#sphx-glr-auto-examples-simple-py
plt.subplots(figsize=(10, 6))
# 设置词云内容
wc443 = WordCloud(stopwords=STOPWORDS.add('Name'),
collocations=False, background_color='black')
text_wc443 = str(dogrates_clean['name'].dropna())
wc443.generate(text_wc443)
# 显示词云
plt.imshow(wc443, interpolation="bilinear")
plt.axis('off')
# 设置大图标题
plt.suptitle('Popular Names', fontweight='bold', y=.93)
show_tweet_pic(column='average', method='max')
dogrates_clean.loc[[dogrates_clean['average'].idxmax()]]
show_tweet_pic(column='average', method='min')
dogrates_clean.loc[[dogrates_clean['average'].idxmin()]]
show_tweet_pic(column='favorite_count', method='max')
dogrates_clean.loc[[dogrates_clean['favorite_count'].idxmax()]]
show_tweet_pic(column='favorite_count', method='min')
dogrates_clean.loc[[dogrates_clean['favorite_count'].idxmin()]]
show_tweet_pic(column='RT/Like', method='max')
dogrates_clean.loc[[dogrates_clean['RT/Like'].idxmax()]]
show_tweet_pic(column='RT/Like', method='min')
dogrates_clean.loc[[dogrates_clean['RT/Like'].idxmin()]]
# 使用热力图绘制数据集各变量之间的相关性状况
fig4430, ax4430 = plt.subplots(figsize=(15, 12))
ax4430 = sns.heatmap(dogrates_clean.corr(), annot=True,
linewidths=.5, cmap=sequential_ui)
# 设置大图标题
plt.suptitle('Correlations between Variables', fontweight='bold', y=.93)
观察:
# 创建临时数据集以准备按月聚合
df451 = dogrates_clean.copy()
# 创建月信息
# https://stackoverflow.com/questions/25146121/extracting-just-month-and-year-from-pandas-datetime-column-python
df451['YearMonth'] = df451['timestamp'].map(
lambda x: 100*x.year + x.month)
# 按上述月信息对数据集进行透视,得出每月发帖量
pivot_tweets_per_month = df451.pivot_table(values=['average', 'tweet_id'],
index=['YearMonth'],
columns=None,
aggfunc={'average': np.mean,
'tweet_id': len})
# 因月度信息需要使用,重设透视表索引
pivot_tweets_per_month = pivot_tweets_per_month.reset_index()
# 检查得到的都是数据集
pivot_tweets_per_month.head(2)
# 使用上述数据集,绘制发帖量的逐月变化情况
# 图形初始化
fig4510, ax4510 = plt.subplots(figsize=(15, 6.2))
# 绘制图形
sns.barplot(x='YearMonth', y='tweet_id',
data=pivot_tweets_per_month, ax=ax4510, color='#00EAD8')
# 去掉不必要的边框
sns.despine()
# 设置轴标题
ax4510.set(ylabel='Count of Tweets', xlabel="Year-Month Timeseries")
# 设置大图标题
plt.suptitle('Tweets per Month since Nov. 2015', fontweight='bold', y=.93)
观察:
# 图形初始化
fig4521, ax4521 = plt.subplots(figsize=(15, 6.2))
# 绘制月平均分数变化图
sns.pointplot(x='YearMonth', y='average',
data=pivot_tweets_per_month, color='#083B5B', ax=ax4521)
# 去除不必要的元素
sns.despine()
# 设置轴标题
ax4521.set(ylabel='Rating Average of the Month', xlabel="Year-Month Timeseries")
# 设置大图标题
plt.suptitle('Rating Average per Month since Nov. 2015',
fontweight='bold', y=.93)
观察:
这可能意味着,WeRateDogs打出突破天际的高分的特色并非一开始就形成的。
# 创建临时数据集以准备按月聚合
# 创建年信息
df451['Year'] = df451['timestamp'].map(
lambda x: x.year)
# 按年聚合每个分数等级的推特计数
pivot_avg_cate_per_year = df451.pivot_table(values=['tweet_id'],
index=['average_cate'],
columns=['Year'],
aggfunc={'tweet_id': len})
# 计算每年每个分数等级占该年推特总数的占比
pivot_avg_cate_pct = pivot_avg_cate_per_year.apply(lambda x: (x/x.sum()*100))
# 进一步处理数据集以便绘图
pivot_avg_cate_pct = pivot_avg_cate_pct.transpose()
pivot_avg_cate_pct.index = pivot_avg_cate_pct.index.droplevel()
# 检视数据集
pivot_avg_cate_pct
# 使用plotly绘制堆积柱状图
# 设定为绘图数据为横向柱状图
data4523 = [go.Bar(y=pivot_avg_cate_pct.index, x=pivot_avg_cate_pct['very_low'], orientation='h', name='Very Low: 0 - 3.5', marker=dict(color="#01B8AA")),
go.Bar(y=pivot_avg_cate_pct.index,
x=pivot_avg_cate_pct['low'], orientation='h', name='Low: 3.5 - 7', marker=dict(color="#374649")),
go.Bar(y=pivot_avg_cate_pct.index,
x=pivot_avg_cate_pct['medium'], orientation='h', name='Medium: 7 - 10', marker=dict(color="#FD625E")),
go.Bar(y=pivot_avg_cate_pct.index, x=pivot_avg_cate_pct['high'], orientation='h', name='High: 10 - 14', marker=dict(color="#F2C80F"))]
# 设定layout为堆积柱状图
layout4523 = go.Layout(
barmode='stack', title='Stacked Percentage of Average Categories, per Year', yaxis=dict(showticklabels=True))
# 初始化图像数据
fig4523 = go.Figure(data=data4523, layout=layout4523)
# 使用离线模式绘图
of.iplot(fig4523)
观察:
# 数据预处理
# 创建临时数据集,处理月度信息,准备绘制反映分数区间的箱型图
df4522 = dogrates_clean.copy()
# 整合年度与月度信息
# https://stackoverflow.com/questions/25146121/extracting-just-month-and-year-from-pandas-datetime-column-python
df4522['YearMonth'] = df4522['timestamp'].map(
lambda x: 100*x.year + x.month)
# ---------------------------------------------------------
# 图形绘制
# 图形初始化
fig4522, ax4522 = plt.subplots(figsize=(25, 7))
# 绘制箱型图
ax4522 = sns.boxplot(
x=df4522['YearMonth'], y=df4522['average'], palette=ui_palette_light)
# 去除不必要的元素
sns.despine()
# 设置轴标题
ax4522.set(ylabel='Rating Average of Tweets', xlabel="Year-Month Timeseries")
# 设置大图标题
plt.suptitle('Ratings Average Distribution, since Nov. 2015',
fontweight='bold', y=.93)
# 创建临时数据集以便进一步处理
df4531 = dogrates_clean.copy()
# 提取日期信息,以将推特点赞和转赞率按日聚合
# https://stackoverflow.com/questions/9962822/pandas-pivot-table-on-date
df4531['dates_of_tweet'] = df4531['timestamp'].map(lambda x: x.date())
# 创建数据透视表,提取每日平均转赞比
pivot_LikeRT = df4531.pivot_table(values=['RT/Like'],
index=['dates_of_tweet'],
columns=None,
aggfunc=np.median)
pivot_LikeRT = pivot_LikeRT.reset_index()
# 直接使用比值数据太割裂,这里使用5天的移动平均值来表明比例的长期变化
# https://stackoverflow.com/questions/40060842/moving-average-pandas
pivot_LikeRT['MA'] = pivot_LikeRT['RT/Like'].rolling(window=5).mean()
pivot_LikeRT.describe()
# 图形绘制初始化
fig4531, ax4531 = plt.subplots(1, 1, sharex=True, figsize=(20, 12))
plt.tight_layout()
ax4533 = ax4531.twinx()
# 绘制每条推特的点赞量和转发量的散点图,并标注里程碑
# 每条推特点赞量
sns.scatterplot(x=dogrates_clean['timestamp'], y=dogrates_clean['favorite_count'].apply(
np.log10), data=dogrates_clean, ax=ax4531, color='#00B8AA')
# 每条推特转发量
sns.scatterplot(x=dogrates_clean['timestamp'], y=dogrates_clean['retweet_count'].apply(
np.log10), data=dogrates_clean, ax=ax4531, color='#F6C66B')
# 点赞量里程碑
sns.scatterplot(x=dogrates_clean['timestamp'], y=dogrates_milestones_favorite['favorite_count'].apply(
np.log10), data=dogrates_milestones_favorite, marker='X', s=100, ax=ax4531, color='#FF000D')
# 点赞量里程碑的转发量
sns.scatterplot(x=dogrates_clean['timestamp'], y=dogrates_milestones_favorite['retweet_count'].apply(
np.log10), data=dogrates_milestones_favorite, marker='^', s=100, ax=ax4531, color='#FF000D')
# --------------------------------------------------------------------------------------------------
# 绘制转赞比的5日移动平均值
# 控制线条粗细
# https://stackoverflow.com/questions/45540886/reduce-line-width-of-seaborn-timeseries-plot
sns.lineplot(x='dates_of_tweet', y='MA', data=pivot_LikeRT,
ax=ax4533, color='#042031', linewidth=1.7)
# 为X轴设定起止日期
# https://stackoverflow.com/questions/21423158/how-do-i-change-the-range-of-the-x-axis-with-datetimes-in-matplotlib
fig4531.autofmt_xdate()
ax4531.set_xlim([datetime.date(2015, 11, 1), datetime.date(2017, 8, 1)])
# 设置图例
# https://stackoverflow.com/questions/48743867/legend-not-showing-when-plotting-multiple-seaborn-plots
fig4531.legend(['Favorite Count', 'Retweet Count', 'Milestone: Favorite Count',
'Milestone: Retweet Count', '5-Day Moving Average of Retweet/Favorite Ratio'], loc='lower right')
sns.despine()
# 设置轴标题
ax4531.set(ylabel='Normalized Levels of Favorites/Retweets', xlabel="Year-Month Timeseries")
# 设置大图标题
plt.suptitle('Scatter Plot for Favorites & Retweets for Tweets since Nov. 2015',
fontweight='bold', y=1)
观察:
df4532 = dogrates_clean.copy()
# https://stackoverflow.com/questions/25146121/extracting-just-month-and-year-from-pandas-datetime-column-python
df4532['YearMonth'] = df4532['timestamp'].map(
lambda x: 100*x.year + x.month)
pivot_tweets_vs_LikeRT = df4532.pivot_table(values=['tweet_id', 'favorite_count', 'retweet_count', 'RT/Like'],
index=['YearMonth'],
columns=None,
aggfunc={'tweet_id': len,
'favorite_count': [sum, np.mean],
'retweet_count': [sum, np.mean],
'RT/Like': np.mean})
pivot_tweets_vs_LikeRT = pivot_tweets_vs_LikeRT.reset_index()
pivot_tweets_vs_LikeRT['favorite_MoM'] = pivot_tweets_vs_LikeRT[(
'favorite_count', 'sum')].pct_change()
pivot_tweets_vs_LikeRT['retweet_MoM'] = pivot_tweets_vs_LikeRT[(
'retweet_count', 'sum')].pct_change()
pivot_tweets_vs_LikeRT['num_of_tweets_MoM'] = pivot_tweets_vs_LikeRT['tweet_id'].pct_change()
pivot_tweets_vs_LikeRT.head()
#
# 改变多图的显示比例
# https://stackoverflow.com/questions/10388462/matplotlib-different-size-subplots
# https://matplotlib.org/users/gridspec.html
fig4532, (ax45321, ax45323) = plt.subplots(2, 1, sharex=True,
figsize=(15, 9), gridspec_kw={'height_ratios': [3, 1]})
ax45322 = ax45321.twinx()
sns.barplot(x='YearMonth', y=('favorite_count', 'sum'),
data=pivot_tweets_vs_LikeRT, ax=ax45321, color='#00EAD8', label='Favorite Sum')
sns.barplot(x='YearMonth', y=('retweet_count', 'sum'),
data=pivot_tweets_vs_LikeRT, ax=ax45321, color='#083B5B', label='Retweet Sum')
sns.pointplot(x='YearMonth', y=('RT/Like', 'mean'),
data=pivot_tweets_vs_LikeRT, ax=ax45322, color='#FD615E', label='Favorite PCT Change MoM')
sns.barplot(x='YearMonth', y='favorite_MoM', data=pivot_tweets_vs_LikeRT, color='#00EAD8', ax=ax45323, label='Favorite PCT Change MoM')
sns.barplot(x='YearMonth', y='retweet_MoM', data=pivot_tweets_vs_LikeRT, color='#083B5B', ax=ax45323, label='Retweet PCT Change MoM')
# https://matplotlib.org/api/_as_gen/matplotlib.axes.Axes.hlines.html
ax45323.axhline(y=0, color='black', linestyle="dashed")
# ax4410.legend()
sns.despine()
# 设置轴标题
ax45321.set(ylabel='Sum of Favorites/Retweets', xlabel="Year-Month Timeseries")
ax45322.set(ylabel='Mean of Retweet/Favorite Ratio', xlabel="Year-Month Timeseries")
ax45323.set(ylabel='PCT Change of Favorites/Retweets, MoM', xlabel="Year-Month Timeseries")
# 设置图例
ax45321.legend(ncol=2, loc="upper right", frameon=True)
ax45323.legend(ncol=2, loc="upper right", frameon=True)
# 设置大图标题
plt.suptitle('Favorites, Retweets, Retweet/Like Ratios, and PCT Change per Month',
fontweight='bold', y=.93)
观察:
#
fig4533, (ax45331, ax45333) = plt.subplots(2, 1, sharex=True,
figsize=(15, 12), gridspec_kw={'height_ratios': [3, 2]})
ax45332 = ax45331.twinx()
sns.barplot(x='YearMonth', y=('favorite_count', 'sum'),
data=pivot_tweets_vs_LikeRT, ax=ax45331, color='#00EAD8', label='Favorite Sum')
sns.barplot(x='YearMonth', y=('retweet_count', 'sum'),
data=pivot_tweets_vs_LikeRT, ax=ax45331, color='#083B5B', label='Retweet Sum')
sns.pointplot(x='YearMonth', y=('tweet_id', 'len'),
data=pivot_tweets_vs_LikeRT, ax=ax45332, color='#FD615E')
sns.barplot(x='YearMonth', y=('favorite_count', 'mean'),
data=pivot_tweets_vs_LikeRT, ax=ax45333, color='#00EAD8', label='Average Favorite')
sns.barplot(x='YearMonth', y=('retweet_count', 'mean'),
data=pivot_tweets_vs_LikeRT, ax=ax45333, color='#083B5B', label='Average Retweet')
sns.despine()
# 设置轴标题
ax45331.set(ylabel='Sum of Favorites/Retweets', xlabel="Year-Month Timeseries")
ax45332.set(ylabel='Count of Tweets', xlabel="Year-Month Timeseries")
ax45333.set(ylabel='Level of Average Favorite/Retweet', xlabel="Year-Month Timeseries")
# 设置图例
ax45331.legend(ncol=2, loc="upper right", frameon=True)
ax45333.legend(ncol=2, loc="upper right", frameon=True)
# 设置大图标题
plt.suptitle('Favorites, Retweets & Retweet/Like Ratios per Month: Total vs. Average',
fontweight='bold', y=.93)
观察:
# 创建临时数据集
df510 = dogrates_clean.copy()
# 提取时间和年份信息
df510['Hour'] = df510['timestamp'].map(
lambda x: x.hour)
df510['Year'] = df510['timestamp'].map(
lambda x: x.year)
# 创建所有推文按发出时间点的据合,并计算该时间点的平均点赞量,转发量和转赞比
pivot_tweets_per_hour = df510.pivot_table(values=['favorite_count', 'retweet_count', 'RT/Like', 'tweet_id'],
index=['Hour'],
columns=None,
aggfunc={'favorite_count': np.mean,
'retweet_count': np.mean,
'tweet_id': len,
'RT/Like': np.mean})
missing_hours = [7, 8, 9, 10, 11, 12]
for num in missing_hours:
pivot_tweets_per_hour.loc[num] = 0
# 重设索引以利用小时
pivot_tweets_per_hour = pivot_tweets_per_hour.reset_index()
# 检视结果
pivot_tweets_per_hour
# 绘制发推时间点及时间点对应的平均点赞、转发和转赞比
fig510, (ax5100, ax5102) = plt.subplots(2, 1, sharex=True,
figsize=(21, 8), gridspec_kw={'height_ratios': [3, 1]})
ax5101 = ax5100.twinx()
# 绘制点赞量&转发量,将二者取10的对数避免极值影响观察
sns.barplot(x=pivot_tweets_per_hour['Hour'], y=pivot_tweets_per_hour['favorite_count'].apply(np.log10),
ax=ax5100, color='#00EAD8', label='Average Favorites')
sns.barplot(x=pivot_tweets_per_hour['Hour'], y=pivot_tweets_per_hour['retweet_count'].apply(np.log10),
ax=ax5100, color='#083B5B', label='Average Retweets')
# 绘制转赞比
sns.pointplot(x='Hour', y='RT/Like', data=pivot_tweets_per_hour,
ax=ax5101, color='#FD615E')
# 绘制各时间点推文数量
sns.pointplot(x='Hour', y='tweet_id', data=pivot_tweets_per_hour,
ax=ax5102, color='#F2C80F')
# fig441.legend(['5-Day Moving Average of Like/Retweet Ratio'], loc='upper left')
fig510.autofmt_xdate()
ax5100.set_ylim(2.5, 5)
ax5101.set_ylim(0.0, 0.55)
ax5102.set_ylim(0, 300)
sns.despine()
# 设置轴标题
ax5100.set(ylabel='Normalized Levels of Favorite/Retweets', xlabel="Hours of a Day")
ax5101.set(ylabel='RT/Like Ratios', xlabel="Hours of a Day")
ax5102.set(ylabel='Number of Tweets', xlabel="Hours of a Day")
# 设置图例
ax5100.legend(ncol=2, loc="upper right", frameon=True)
# 设置大图标题
plt.suptitle('Average Favorites, Retweets & Retweet/Like Ratios, with Number of Tweets Sent per Hour',
fontweight='bold', y=.93)
# 创建所有推文按发出年份和时间点的聚合,并计算该时间点的平均点赞量,转发量和转赞比
pivot_tweets_per_hour_per_year = df510.pivot_table(values=['favorite_count', 'retweet_count', 'RT/Like', 'tweet_id'],
index=['Year', 'Hour'],
columns=None,
aggfunc={'favorite_count': np.mean,
'retweet_count': np.mean,
'tweet_id': len,
'RT/Like': np.mean})
# 为每年的发推时间点数据建立单独的数据集,并补全缺失的小时
# 2015 -------------------------------------------------------------------
pivot_tweets_per_hour_2015 = pivot_tweets_per_hour_per_year.loc[2015]
pivot_tweets_per_hour_2015 = pivot_tweets_per_hour_2015.copy() # 复制一下,不然有警告
for num in missing_hours:
pivot_tweets_per_hour_2015.loc[num] = 0
# 2016 -------------------------------------------------------------------
pivot_tweets_per_hour_2016 = pivot_tweets_per_hour_per_year.loc[2016]
pivot_tweets_per_hour_2016 = pivot_tweets_per_hour_2016.copy() # 复制一下,不然有警告
num2016 = [7, 8, 9, 10, 11, 12, 13]
for num in num2016:
pivot_tweets_per_hour_2016.loc[num] = 0
# 2017 -------------------------------------------------------------------
pivot_tweets_per_hour_2017 = pivot_tweets_per_hour_per_year.loc[2017]
pivot_tweets_per_hour_2017 = pivot_tweets_per_hour_2017.copy() # 复制一下,不然有警告
num2017 = [5, 6, 7, 8, 9, 10, 11, 12, 13]
for num in num2017:
pivot_tweets_per_hour_2017.loc[num] = 0
# 绘制发推时间点及时间点对应的平均点赞、转发和转赞比
fig511, ax5110 = plt.subplots(2, 3,
figsize=(21, 8), gridspec_kw={'height_ratios': [4, 1]})
# 2015 ------------------------------------------------------------------------------------------------------------------------------
# 绘制2015年点赞量&转发量,将二者取10的对数避免极值影响观察
sns.barplot(x=pivot_tweets_per_hour_2015.index, y=pivot_tweets_per_hour_2015['favorite_count'].apply(
np.log10), ax=ax5110[0, 0], color='#00EAD8')
sns.barplot(x=pivot_tweets_per_hour_2015.index, y=pivot_tweets_per_hour_2015['retweet_count'].apply(
np.log10), ax=ax5110[0, 0], color='#083B5B')
# 绘制转赞比
ax511a = ax5110[0, 0].twinx()
sns.pointplot(x=pivot_tweets_per_hour_2015.index, y=pivot_tweets_per_hour_2015['RT/Like'],
ax=ax511a, color='#FD615E')
# 绘制各时间点推文数量
sns.pointplot(x=pivot_tweets_per_hour_2015.index,
y=pivot_tweets_per_hour_2015['tweet_id'], ax=ax5110[1, 0], color='#F2C80F')
# 2016 ------------------------------------------------------------------------------------------------------------------------------
# 绘制2016年点赞量&转发量,将二者取10的对数避免极值影响观察
sns.barplot(x=pivot_tweets_per_hour_2016.index, y=pivot_tweets_per_hour_2016['favorite_count'].apply(
np.log10), ax=ax5110[0, 1], color='#00EAD8')
sns.barplot(x=pivot_tweets_per_hour_2016.index, y=pivot_tweets_per_hour_2016['retweet_count'].apply(
np.log10), ax=ax5110[0, 1], color='#083B5B')
# 绘制转赞比
ax511b = ax5110[0, 1].twinx()
sns.pointplot(x=pivot_tweets_per_hour_2016.index, y=pivot_tweets_per_hour_2016['RT/Like'],
ax=ax511b, color='#FD615E')
# 绘制各时间点推文数量
sns.pointplot(x=pivot_tweets_per_hour_2016.index,
y=pivot_tweets_per_hour_2016['tweet_id'], ax=ax5110[1, 1], color='#F2C80F')
# 2017 ------------------------------------------------------------------------------------------------------------------------------
# 绘制2017年点赞量&转发量,将二者取10的对数避免极值影响观察
sns.barplot(x=pivot_tweets_per_hour_2017.index, y=pivot_tweets_per_hour_2017['favorite_count'].apply(
np.log10), ax=ax5110[0, 2], color='#00EAD8', label = 'Favorites')
sns.barplot(x=pivot_tweets_per_hour_2017.index, y=pivot_tweets_per_hour_2017['retweet_count'].apply(
np.log10), ax=ax5110[0, 2], color='#083B5B', label = 'Retweets')
# 绘制转赞比
ax511c = ax5110[0, 2].twinx()
sns.pointplot(x=pivot_tweets_per_hour_2017.index, y=pivot_tweets_per_hour_2017['RT/Like'],
ax=ax511c, color='#FD615E')
# 绘制各时间点推文数量
sns.pointplot(x=pivot_tweets_per_hour_2017.index,
y=pivot_tweets_per_hour_2017['tweet_id'], ax=ax5110[1, 2], color='#F2C80F')
# 其他设置 -------------------------------------------------------------------------------------------------------------------------
# fig441.legend(['5-Day Moving Average of Like/Retweet Ratio'], loc='upper left')
# fig511.autofmt_xdate()
# 统一反映点赞量和转发量的柱状图坐标轴,去掉不影响观察的部分(低于2.5的部分)以突出变化情况
ax5110[0, 0].set_ylim(2.5, 5.5)
ax5110[0, 1].set_ylim(2.5, 5.5)
ax5110[0, 2].set_ylim(2.5, 5.5)
# 统一反映专注比的次坐标轴
ax511a.set_ylim(0, 0.6)
ax511b.set_ylim(0, 0.6)
ax511c.set_ylim(0, 0.6)
# 统一反映发帖量的折线图坐标轴
ax5110[1, 0].set_ylim(0, 160)
ax5110[1, 1].set_ylim(0, 160)
ax5110[1, 2].set_ylim(0, 160)
# 设置轴标题
ax5110[0, 0].set(ylabel='Normalized Levels of Favorites/Retweets', xlabel='')
ax5110[0, 1].set(ylabel='', xlabel='')
ax5110[0, 2].set(ylabel='', xlabel='')
ax5110[1, 0].set(ylabel='Number of Tweets', xlabel='Hours of a Day, 2015')
ax5110[1, 1].set(ylabel='', xlabel='Hours of a Day, 2016')
ax5110[1, 2].set(ylabel='', xlabel='Hours of a Day, 2017')
# 为柱状图设置图例
ax5110[0, 2].legend(loc='upper right')
# 设置图例
ax5100.legend(ncol=2, loc="upper right", frameon=True)
sns.despine()
# 设置大图标题
plt.suptitle('Average Favorites, Retweets & Retweet/Like Ratios, with Number of Tweets Sent per Hour per Year',
fontweight='bold', y=.93)
观察:
总体来看,每小时的推文数量确实有波动,但这不代表某个小时的创造力就是比别的时间段高,也很难证明某几个小时的推文就是比其他的时间段的推文受欢迎。对这个方向,我们不做进一步的探究。
# 创建临时数据集
df520 = dogrates_clean.copy()
# 提取时间和年份信息
df520['Weekday'] = df520['timestamp'].map(
lambda x: x.dayofweek)
df520['Year'] = df520['timestamp'].map(
lambda x: x.year)
df520.head(1)
# 创建所有推文按发出时间点的据合,并计算该时间点的平均点赞量,转发量和转赞比
pivot_tweets_per_weekday = df520.pivot_table(values=['favorite_count', 'retweet_count', 'RT/Like', 'tweet_id'],
index=['Weekday'],
columns=None,
aggfunc={'favorite_count': np.mean,
'retweet_count': np.mean,
'tweet_id': len,
'RT/Like': np.mean})
# 重设索引以利用小时
pivot_tweets_per_weekday = pivot_tweets_per_weekday.reset_index()
# 检视结果
pivot_tweets_per_weekday
# 绘制发推时间点及时间点对应的平均点赞、转发和转赞比
fig520, (ax5200, ax5202) = plt.subplots(2, 1, sharex=True,
figsize=(21, 8), gridspec_kw={'height_ratios': [3, 1]})
ax5201 = ax5200.twinx()
# 绘制点赞量&转发量,将二者取10的对数避免极值影响观察
sns.barplot(x=pivot_tweets_per_weekday['Weekday'], y=pivot_tweets_per_weekday['favorite_count'].apply(np.log10),
ax=ax5200, color='#00EAD8')
sns.barplot(x=pivot_tweets_per_weekday['Weekday'], y=pivot_tweets_per_weekday['retweet_count'].apply(np.log10),
ax=ax5200, color='#083B5B')
# 绘制转赞比
sns.pointplot(x='Weekday', y='RT/Like', data=pivot_tweets_per_weekday,
ax=ax5201, color='#FD615E')
# 绘制各时间点推文数量
sns.pointplot(x='Weekday', y='tweet_id', data=pivot_tweets_per_weekday,
ax=ax5202, color='#F2C80F')
# fig441.legend(['5-Day Moving Average of Like/Retweet Ratio'], loc='upper left')
fig520.autofmt_xdate()
ax5200.set_ylim(3.0, 4.5)
ax5201.set_ylim(0.3, 0.4)
ax5202.set_ylim(200, 350)
sns.despine()
# 设置大图标题
plt.suptitle('Average Favorites, Retweets & Retweet/Like Ratios, with Number of Tweets Sent per Weekday',
fontweight='bold', y=.93)
# 创建所有推文按发出年份和时间点的聚合,并计算该时间点的平均点赞量,转发量和转赞比
pivot_tweets_per_weekday_per_year = df520.pivot_table(values=['favorite_count', 'retweet_count', 'RT/Like', 'tweet_id'],
index=['Year', 'Weekday'],
columns=None,
aggfunc={'favorite_count': np.mean,
'retweet_count': np.mean,
'tweet_id': len,
'RT/Like': np.mean})
# 为每年的发推时间点数据建立单独的数据集,并补全缺失的小时
# 2015 -------------------------------------------------------------------
pivot_tweets_per_weekday_2015 = pivot_tweets_per_weekday_per_year.loc[2015]
pivot_tweets_per_weekday_2015 = pivot_tweets_per_weekday_2015.copy() # 复制一下,不然有警告
# 2016 -------------------------------------------------------------------
pivot_tweets_per_weekday_2016 = pivot_tweets_per_weekday_per_year.loc[2016]
pivot_tweets_per_weekday_2016 = pivot_tweets_per_weekday_2016.copy() # 复制一下,不然有警告
# 2017 -------------------------------------------------------------------
pivot_tweets_per_weekday_2017 = pivot_tweets_per_weekday_per_year.loc[2017]
pivot_tweets_per_weekday_2017 = pivot_tweets_per_weekday_2017.copy() # 复制一下,不然有警告
# 绘制发推时间点及时间点对应的平均点赞、转发和转赞比
fig521, ax5210 = plt.subplots(2, 3,
figsize=(21, 8), gridspec_kw={'height_ratios': [4, 1]})
# 2015 ------------------------------------------------------------------------------------------------------------------------------
# 绘制2015年点赞量&转发量,将二者取10的对数避免极值影响观察
sns.barplot(x=pivot_tweets_per_weekday_2015.index, y=pivot_tweets_per_weekday_2015['favorite_count'].apply(
np.log10), ax=ax5210[0, 0], color='#00EAD8')
sns.barplot(x=pivot_tweets_per_weekday_2015.index, y=pivot_tweets_per_weekday_2015['retweet_count'].apply(
np.log10), ax=ax5210[0, 0], color='#083B5B')
# 绘制转赞比
ax521a = ax5210[0, 0].twinx()
sns.pointplot(x=pivot_tweets_per_weekday_2015.index, y=pivot_tweets_per_weekday_2015['RT/Like'],
ax=ax521a, color='#FD615E')
# 绘制各时间点推文数量
sns.pointplot(x=pivot_tweets_per_weekday_2015.index,
y=pivot_tweets_per_weekday_2015['tweet_id'], ax=ax5210[1, 0], color='#F2C80F')
# 2016 ------------------------------------------------------------------------------------------------------------------------------
# 绘制2016年点赞量&转发量,将二者取10的对数避免极值影响观察
sns.barplot(x=pivot_tweets_per_weekday_2016.index, y=pivot_tweets_per_weekday_2016['favorite_count'].apply(
np.log10), ax=ax5210[0, 1], color='#00EAD8')
sns.barplot(x=pivot_tweets_per_weekday_2016.index, y=pivot_tweets_per_weekday_2016['retweet_count'].apply(
np.log10), ax=ax5210[0, 1], color='#083B5B')
# 绘制转赞比
ax521b = ax5210[0, 1].twinx()
sns.pointplot(x=pivot_tweets_per_weekday_2016.index, y=pivot_tweets_per_weekday_2016['RT/Like'],
ax=ax521b, color='#FD615E')
# 绘制各时间点推文数量
sns.pointplot(x=pivot_tweets_per_weekday_2016.index,
y=pivot_tweets_per_weekday_2016['tweet_id'], ax=ax5210[1, 1], color='#F2C80F')
# 2017 ------------------------------------------------------------------------------------------------------------------------------
# 绘制2017年点赞量&转发量,将二者取10的对数避免极值影响观察
sns.barplot(x=pivot_tweets_per_weekday_2017.index, y=pivot_tweets_per_weekday_2017['favorite_count'].apply(
np.log10), ax=ax5210[0, 2], color='#00EAD8', label='Favorites')
sns.barplot(x=pivot_tweets_per_weekday_2017.index, y=pivot_tweets_per_weekday_2017['retweet_count'].apply(
np.log10), ax=ax5210[0, 2], color='#083B5B', label='Retweets')
# 绘制转赞比
ax521c = ax5210[0, 2].twinx()
sns.pointplot(x=pivot_tweets_per_weekday_2017.index, y=pivot_tweets_per_weekday_2017['RT/Like'],
ax=ax521c, color='#FD615E')
# 绘制各时间点推文数量
sns.pointplot(x=pivot_tweets_per_weekday_2017.index,
y=pivot_tweets_per_weekday_2017['tweet_id'], ax=ax5210[1, 2], color='#F2C80F')
# 其他设置 -------------------------------------------------------------------------------------------------------------------------
# fig441.legend(['5-Day Moving Average of Like/Retweet Ratio'], loc='upper left')
# fig511.autofmt_xdate()
# 统一反映点赞量和转发量的柱状图坐标轴,去掉不影响观察的部分(低于2.5的部分)以突出变化情况
ax5210[0, 0].set_ylim(2.5, 5)
ax5210[0, 1].set_ylim(2.5, 5)
ax5210[0, 2].set_ylim(2.5, 5)
# 统一反映专注比的次坐标轴
ax521a.set_ylim(0, 0.5)
ax521b.set_ylim(0, 0.5)
ax521c.set_ylim(0, 0.5)
# 统一反映发帖量的折线图坐标轴
ax5210[1, 0].set_ylim(0, 200)
ax5210[1, 1].set_ylim(0, 200)
ax5210[1, 2].set_ylim(0, 200)
sns.despine()
# 设置轴标题
ax5210[0, 0].set(ylabel='Normalized Levels of Favorites/Retweets', xlabel='')
ax5210[0, 1].set(ylabel='', xlabel='')
ax5210[0, 2].set(ylabel='', xlabel='')
ax5210[1, 0].set(ylabel='Number of Tweets', xlabel='Day of Week, 2015')
ax5210[1, 1].set(ylabel='', xlabel='Day of Week, 2016')
ax5210[1, 2].set(ylabel='', xlabel='Day of Week, 2017')
# 为柱状图设置图例
ax5210[0, 2].legend(loc='upper right')
# 设置大图标题
plt.suptitle('Average Favorites, Retweets & Retweet/Like Ratios, with Number of Tweets Sent per Weekday per Year',
fontweight='bold', y=.93)
观察:
总体来看看不出发帖星期相关的因素。我们不再进一步研究相关内容。
pivot_breeds_plotting_enhanced = pivot_breeds_enhanced.reset_index().head(10)
pivot_breeds_plotting_enhanced
# 'https://seaborn.pydata.org/examples/pairgrid_dotplot.html'
sns.set(style='whitegrid')
fig610 = sns.PairGrid(pivot_breeds_plotting_enhanced, palette=ui_palette_light, x_vars=[('favorite_count', 'mean'), ('retweet_count', 'mean'), ('RT/Like', 'mean'),
('tweet_id', 'len')], y_vars=['breed'], height=8, aspect=.35)
fig610 = fig610.map(sns.barplot, orient='h', edgecolor='w',
palette=ui_palette_light)
# sns.despine(bottom=True, left=True)
# 设定标题,轴等
titles = ['Favorites Avg.', 'Retweets Avg.',
'RT/Like Ratio', 'Number of Tweets']
for ax, title in zip(fig610.axes.flat, titles):
# 为每个轴单独设定标题
ax.set(title=title)
# 取消纵向网格线,改为水平网格线
ax.xaxis.grid(False)
ax.yaxis.grid(True)
# 设置大图标题
plt.suptitle('Details of Most Favored Breeds', fontweight='bold', y=1.04)
经过上面的一系列操作,我们得到了平均点赞量前10的狗狗品种(点赞量由高到低排序)。
从上图看来,萨卢基猎犬收获的平均点赞量最多,其次是法国斗牛犬和阿富汗猎犬;但在转赞比上,阿富汗猎犬,斑点狗和标准贵宾犬的转赞比较高。另外,萨卢基猎犬,阿富汗猎犬等较为稀有的品种推特数量也较少。
我们想问:有没有狗狗品种收获的平均点赞量显著的超越其他的品种呢?有没有狗狗的品种有显著的粉丝群体呢?想要解答这两个问题,我们将跟别对上图挑选出来的10各品种的点赞量和转赞比分别做因此方差分析。
推主Marr Nelson在表述狗狗的时候发明了如下词汇:
这些词汇主要用于形容不同狗狗的生长状态。从词汇构成的角度来看,其主要由一个主词汇和后缀变体组成(例如doggo = dog + go, pupper = pup + per),营造出一种喜庆但略显低龄化的语言现象(待补充)。上述所谓的“简单理解”,是指在尽可能不曲解原意的情况下,抛弃作者营造的一切搞笑成分,并尽量压缩理解难度。总而言之言而总之一言以蔽之,这个分类比较随意,亲爱的读者你大概知道这么个意思就行。
我们不禁感到好奇:小狗会更受欢迎吗?不同的分类之间到底有没有受到喜爱程度的区别呢?如果有,多大呢?我们先用箱型图展示一下不同分类之间的点赞量额转赞比的分布情况。
# 绘制不同分类的点赞量和转赞比分布情况
fig620, (ax6200, ax6201) = plt.subplots(1, 2, figsize=(20, 7))
# 绘制点赞量分布图
sns.boxenplot(x=dogrates_clean['stage(s)'], y=dogrates_clean['favorite_count'].apply(np.log10), palette = ui_palette_light, ax=ax6200)
# 绘制转赞比分布图
sns.boxenplot(x=dogrates_clean['stage(s)'], y=dogrates_clean['RT/Like'].apply(np.log), palette = ui_palette_light, ax=ax6201)
sns.despine()
# 设置轴标题
ax6200.set(ylabel='Normalized Levels of Favorites', xlabel="Stages")
ax6201.set(ylabel='Normalized Levels of Retweet/Favorite Ratios', xlabel="Stages")
# 设置大图标题
plt.suptitle('Stage Difference on Favorites and RT/Like Ratios (Normalized)', fontweight='bold', y=.93)
诚然,推主在每条推特中对狗狗的打分都是出于娱乐目的。但分数越高,确实越有可能代表着狗狗的故事更加搞笑/欢乐,进而带来更多的点赞和转发。另外,分数低的推文并不代表魅力全无,其很可能是故意为之;比如这副😆:
# example 3: Not Dog
show_tweet_pic(method='tweet_id', tweet_id = 675153376133427200)
因此,探索不同分数等级的点赞量和转赞比情况,我们有可能能够发现更受欢迎的内容。我们将首先直观的绘制不同分数等级的分布情况;随后,我们将绘制每个分数对应的点赞量和转赞,观察其是否有潜在的线性关系;最后,我们将探索分数等级之间的均值是否相同,进而判断不同的分数等级之间是否拥有足够的差异。
# 绘制不同分数等级的分布情况的散点图,以不同颜色表达不同分数等级
fig630, ax630 = plt.subplots(figsize=(16.2, 10))
ax630 = sns.scatterplot(x=dogrates_clean['favorite_count'].apply(np.log10), y=dogrates_clean['retweet_count'].apply(np.log10), hue='average_cate',
size='average_cate', sizes=(20, 200), data=dogrates_clean, palette=["#01B8AA", "#374649", "#FD625E", "#F2C80F"])
sns.despine()
# 设置轴标题
ax630.set(ylabel='Normalized Levels of Retweets', xlabel="Normalized Levels of Favorites")
ax630.legend()
# 设置大图标题
plt.suptitle('Scattered Favorites & Retweets for Average Categories (Normalized)',
fontweight='bold', y=.93)
fig631, (ax6310, ax6311) = plt.subplots(1, 2, figsize=(21, 7))
sns.regplot(x=dogrates_clean['average'], y=dogrates_clean['favorite_count'].apply(
np.log10), color='#01B8AA', ax=ax6310)
sns.regplot(x=dogrates_clean['average'],
y=dogrates_clean['RT/Like'].apply(np.log), color='#374649', ax=ax6311)
sns.despine()
# 设置轴标题
ax6310.set(ylabel='Normalized Levels of Favorites', xlabel="Average Ratings")
ax6311.set(ylabel='Normalized Levels of Retweet/Favorite Ratios', xlabel="Average Ratings")
# 设置大图标题
plt.suptitle('Scattered Favorites & RT/Like Ratios for Average Ratings (Normalized)',
fontweight='bold', y=.93)
# 绘制不同分类的点赞量和转赞比分布情况
fig632, (ax6320, ax6321) = plt.subplots(1, 2, figsize=(20, 7))
# 绘制点赞量分布图
sns.boxenplot(x=dogrates_clean['average_cate'], y=dogrates_clean['favorite_count'].apply(
np.log10), palette=ui_palette_light, ax=ax6320)
# 绘制转赞比分布图
sns.boxenplot(x=dogrates_clean['average_cate'], y=dogrates_clean['RT/Like'].apply(
np.log), palette=ui_palette_light, ax=ax6321)
sns.despine()
# 设置轴标题
ax6320.set(ylabel='Normalized Levels of Favorites', xlabel="Average Categories")
ax6321.set(ylabel='Normalized Levels of Retweet/Favorite Ratios', xlabel="Average Categories")
# 设置大图标题
plt.suptitle('Distribution of Favorites & Retweets for Average Categories (Normalized)',
fontweight='bold', y=.93)
观察上三张图可知:
我们不禁要问:点赞量和转赞比的关系究竟是怎样的?这种关系能够多大程度上反映实际情况?我们将首先利用线性回归模型来尝试解答这一疑问。
但是,中了线性回归的结果显然是不够的。正如fig632所示,当我们将分数分段,不同分数段之间的点赞量和转赞比也是有区别的。哪个分数段的记过与其他显著的不同?上述观察能否在统计上站住脚?这个结论效应量多大呢?我们还需要进行单因素方差分析来进一步加强我们的结果。
# 绘制不同分类的点赞量和转赞比分布情况
fig640, (ax6400, ax6401) = plt.subplots(1, 2, figsize=(20, 7))
# 绘制点赞量分布图
sns.boxenplot(x=dogrates_clean['dog?'], y=dogrates_clean['favorite_count'].apply(
np.log10), palette=ui_palette_light, ax=ax6400)
# 绘制转赞比分布图
sns.boxenplot(x=dogrates_clean['dog?'], y=dogrates_clean['RT/Like'].apply(
np.log), palette=ui_palette_light, ax=ax6401)
sns.despine()
# 设置轴标题
ax6400.set(ylabel='Normalized Levels of Favorites', xlabel="Possibility Groups of being Dog")
ax6401.set(ylabel='Normalized Levels of Retweet/Favorite Ratios', xlabel="Possibility Groups of being Dog")
# 设置大图标题
plt.suptitle('Distribution of Favorites & RT/Like Ratios for Possibility Levels of Tweets Talking About Dogs (Normalized)', fontweight='bold', y=.93)
仔细观察箱型图发现,推文主题是否是狗的分类在点赞量和转赞比上的均值分布差距不算大。看来这一因素跟推文的受欢迎程度并无强烈的相关关系,我们不做深入探索。
# 绘制性别的点赞量与转赞比的散点/多重线性回归图
ax6500 = sns.lmplot(x='average', y='favorite_count', hue='gender',
data=dogrates_clean, palette=ui_palette_light, height=6)
# 设置轴标题
ax6500.set(ylabel='Normalized Levels of Favorites', xlabel="Average Ratings")
# 设置大图标题
plt.suptitle('Distribution of Favorites for Average Ratings',
fontweight='bold', y=1.02)
ax6501 = sns.lmplot(x='average', y='RT/Like', hue='gender',
data=dogrates_clean, palette=ui_palette_light, height=6)
# 设置轴标题
ax6501.set(ylabel='Normalized Levels of Retweet/Favorite Ratios', xlabel="Average Ratings")
# 设置大图标题
plt.suptitle('Distribution of RT/Like Ratios for Average Ratings',
fontweight='bold', y=1.02)
# 绘制不同分类的点赞量和转赞比分布情况
fig651, (ax6510, ax6511) = plt.subplots(1, 2, figsize=(20, 7))
# 绘制点赞量分布图
sns.boxenplot(x=dogrates_clean['gender'], y=dogrates_clean['favorite_count'].apply(
np.log10), palette=ui_palette_light, ax=ax6510)
# 绘制转赞比分布图
sns.boxenplot(x=dogrates_clean['gender'], y=dogrates_clean['RT/Like'].apply(
np.log), palette=ui_palette_light, ax=ax6511)
sns.despine()
# 设置轴标题
ax6510.set(ylabel='Normalized Levels of Favorites', xlabel="Gender Categories")
ax6511.set(ylabel='Normalized Levels of Retweet/Favorite Ratios', xlabel="Gender Categories")
# 设置大图标题
plt.suptitle('Distribution of Favorites & RT/Like Ratios for Gender Groups',
fontweight='bold', y=.95)
从fig650和fig651看来,推文主体的不同性别确实有点赞数和专注那笔之间的差异,但其差异并不大(箱型图中差距没有超过0.5)。就算我们进一步分析,结论很可能不显著,甚至可能比较偏激(例如雌性主体能吸引更多点赞量是为什么?)。我们不会对这一分类做更进一步的探索。
# 'https://seaborn.pydata.org/examples/pairgrid_dotplot.html'
sns.set(style='whitegrid')
fig610 = sns.PairGrid(pivot_breeds_plotting_enhanced, palette=ui_palette_light,
x_vars=[('favorite_count', 'mean'), ('retweet_count', 'mean'), ('RT/Like', 'mean'),
('tweet_id', 'len')], y_vars=['breed'], height=8, aspect=.35)
fig610 = fig610.map(sns.barplot, orient='h', edgecolor='w',
palette=ui_palette_light)
# sns.despine(bottom=True, left=True)
# 设定标题,轴等
titles = ['Favorites Avg.', 'Retweets Avg.',
'RT/Like Ratio', 'Number of Tweets']
for ax, title in zip(fig610.axes.flat, titles):
# 为每个轴单独设定标题
ax.set(title=title)
# 取消纵向网格线,改为水平网格线
ax.xaxis.grid(False)
ax.yaxis.grid(True)
# 设置大图标题
plt.suptitle('Details of Most Favored Breeds', fontweight='bold', y=1.04)
单因素方差分析非常适合用来通过两个组别的样本判断其总体的平均数是否一致。
在这个方差分析中,我们的零假设(H0)是:这10个品种的狗狗的总体的点赞量的平均数是一致的;若分别用μ1, μ2等表示这些品种的平均点赞量,则μ1=μ2=...=μ9=μ10;我们的备择假设(H1)是:这10个品种中至少有两个品种的平均数不一致。
进行方差分析有三个前提条件:
对于以上三个假设:
理论上,如果以上三个假设条件中任意一个不成立,我们就不能用参数性的单因素方差分析,而需要转而使用Kruskal-Wallis 单因子方差分析。但是,因为返回的H值符合卡方分布,Kruskal-Wallis 单因子方差分析要求最小样本在5个以上,而根据我们的数据透视表,点赞量平均数前10名的狗狗,只有5种符合要求。因此,如果可能,我们还是使用传统的参数性单因素方差分析,因为同样有研究指出,当样本容量足够大的时候,正态性对单因素方差分析影响较小。
在所有的假设检验中,我们所有的置信水平都选择95%,也即α值为0.05。
我们首先检验正态性。在正态性检验中,我们的零假设(H0)是:点赞数量取10的对数后呈正态分布;我们的备择假设(H1)是:点赞数取10的对数后不呈正态分布。我们将使用scipy中statsmodel的normaltest执行这一检验。
# 参考 https://blog.csdn.net/cyan_soul/article/details/81236124?utm_source=blogxgwz8
s, p = stats.normaltest(dogrates_clean['favorite_count'].apply(np.log10))
if p < 0.05:
print('应拒绝零假设,样本分布不呈正态性')
else:
print('拒绝零假设失败,继续')
fig441
# 好吧,确实不咋正态
尴尬😥
我们将使用Levene检验测试品种之间点赞量的方差齐性。因为样本总体不呈正态,我们将使用默认的中值作为测试的数据。
我们的零假设(H0)是:所有的样本来自方差一致的总体。我们的备择假设(H1)是:所有样本来自的总体方差互不一致。
# 将需要检测的数据分组
favorite_test = [dogrates_clean[dogrates_clean['breed'] == 'Saluki']['favorite_count'].apply(np.log10),
dogrates_clean[dogrates_clean['breed'] ==
'French_bulldog']['favorite_count'].apply(np.log10),
dogrates_clean[dogrates_clean['breed'] ==
'Afghan_hound']['favorite_count'].apply(np.log10),
dogrates_clean[dogrates_clean['breed'] ==
'black-and-tan_coonhound']['favorite_count'].apply(np.log10),
dogrates_clean[dogrates_clean['breed'] ==
'flat-coated_retriever']['favorite_count'].apply(np.log10),
dogrates_clean[dogrates_clean['breed'] ==
'Irish_water_spaniel']['favorite_count'].apply(np.log10),
dogrates_clean[dogrates_clean['breed'] ==
'standard_poodle']['favorite_count'].apply(np.log10),
dogrates_clean[dogrates_clean['breed'] ==
'English_springer']['favorite_count'].apply(np.log10),
dogrates_clean[dogrates_clean['breed'] ==
'Cardigan']['favorite_count'].apply(np.log10),
dogrates_clean[dogrates_clean['breed'] == 'Leonberg']['favorite_count'].apply(np.log10)]
# 参考链接 https://pythonfordatascience.org/anova-python/
s, p = stats.levene(*favorite_test)
if p < 0.05:
print('应拒绝零假设,样本不具方差齐性')
else:
print('拒绝零假设失败, 继续')
上述检测完成,样本不呈正态分布,不过是符合方差齐性的。因此,我们对筛选出的10个品种的点赞量进行非参数性的Kruskal-Wallis单因子方差分析。
# 参考链接
# https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.f_oneway.html#scipy.stats.f_oneway
# https://pythonfordatascience.org/anova-python/
stats.f_oneway(*favorite_test)
对挑出的10个平均点赞量最多的狗狗品种进行的单因素方差分析,得出了F值为1.2714260977031635,P值为0.2673514594596377,远远大于我们设定的α值0.05。
这意味着,我们推翻零假设失败,人们对不同品种狗狗的点赞量并没有统计学意义上的不同。我们在图形中展示的平均点赞量的前10名,其平均点赞量的差异是由于人们对这些品种有不同的偏好产生的的假设,没有统计学依据。
我们首先检验正态性。在正态性检验中,我们的零假设(H0)是:点赞数量取10的对数后呈正态分布;我们的备择假设(H1)是:点赞数取10的对数后不呈正态分布。我们将使用scipy中statsmodel的normaltest执行这一检验。
# 参考 https://blog.csdn.net/cyan_soul/article/details/81236124?utm_source=blogxgwz8
s, p = stats.normaltest(dogrates_clean['RT/Like'].apply(np.log))
if p < 0.05:
print('应拒绝零假设,样本分布不呈正态性')
else:
print('拒绝零假设失败,继续')
fig442
# 看右边那个
# 好吧,确实不咋正态
尴尬😥
我们将使用Levene检验测试品种之间转赞比的方差齐性。因为样本总体不呈正态,我们将使用默认的中值作为测试的数据。
我们的零假设(H0)是:所有的样本来自方差一致的总体。我们的备择假设(H1)是:所有样本来自的总体方差互不一致。
RTLikeRatio_test = [dogrates_clean[dogrates_clean['breed'] == 'Saluki']['RT/Like'].apply(np.log),
dogrates_clean[dogrates_clean['breed'] ==
'French_bulldog']['RT/Like'].apply(np.log),
dogrates_clean[dogrates_clean['breed'] ==
'Afghan_hound']['RT/Like'].apply(np.log),
dogrates_clean[dogrates_clean['breed'] ==
'black-and-tan_coonhound']['RT/Like'].apply(np.log),
dogrates_clean[dogrates_clean['breed'] ==
'flat-coated_retriever']['RT/Like'].apply(np.log),
dogrates_clean[dogrates_clean['breed'] ==
'Irish_water_spaniel']['RT/Like'].apply(np.log),
dogrates_clean[dogrates_clean['breed'] ==
'standard_poodle']['RT/Like'].apply(np.log),
dogrates_clean[dogrates_clean['breed'] ==
'English_springer']['RT/Like'].apply(np.log),
dogrates_clean[dogrates_clean['breed'] ==
'Cardigan']['RT/Like'].apply(np.log),
dogrates_clean[dogrates_clean['breed'] == 'Leonberg']['RT/Like'].apply(np.log)]
# 参考链接 https://pythonfordatascience.org/anova-python/
s, p = stats.levene(*RTLikeRatio_test)
if p < 0.05:
print('应拒绝零假设,样本不具方差齐性')
else:
print('拒绝零假设失败, 继续')
上述检测完成,样本不呈正态分布,不过是符合方差齐性的。因此,我们对筛选出的10个品种的转赞比进行非参数性的Kruskal-Wallis单因子方差分析。
# https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.f_oneway.html#scipy.stats.f_oneway
stats.stats.f_oneway(*RTLikeRatio_test)
对挑出的10个平均转赞比最多的狗狗品种进行的单因素方差分析,得出了F值为5.13022058435709,P值为0.8228120104986223,远远大于我们设定的α值0.05。
这意味着,我们推翻零假设失败,人们对不同品种狗狗的喜好程度(反映为转赞比)并没有统计学意义上的不同。我们在图形中展示的平均点赞量的前10名,其平均转赞比的差异是由于人们对这些品种有不同的偏好产生的的假设,没有统计学依据。
根据上述统计学检验,我们最终确认:人们对不同品种的点赞量并没有统计学意义上的不同
fig620
# 复制数据集,调整列名,标准化数据以更好地进行方差分析
df620 = dogrates_clean.copy()
df620 = df620.rename(columns={'stage(s)': 'stages'})
df620['RL_Ratio_log'] = df620['RT/Like'].apply(np.log)
df620['favorite_log'] = df620['favorite_count'].apply(np.log10)
df620.head(1)
进行方差分析有三个前提条件:
对于以上三个假设:
理论上,如果以上三个假设条件中任意一个不成立,我们就不能用参数性的单因素方差分析,而需要转而使用Kruskal-Wallis 单因素方差分析。
在所有的假设检验中,我们所有的置信水平都选择95%,也即α值为0.05。
favorite_test_stages = [df620[df620['stages'] == 'Not Specified']['favorite_log'],
df620[df620['stages'] == 'doggo']['favorite_log'],
df620[df620['stages'] == 'puppo']['favorite_log'],
df620[df620['stages'] == 'pupper']['favorite_log'],
df620[df620['stages'] == 'floofer']['favorite_log'],
df620[df620['stages'] == 'doggo & pupper']['favorite_log']]
# 参考链接 https://pythonfordatascience.org/anova-python/
s, p = stats.levene(*favorite_test_stages)
if p < 0.05:
print('应拒绝零假设,样本不具方差齐性')
else:
print('拒绝零假设失败, 继续')
因为样本不具齐方差性,我们必须使用Kruskal Wallis 单因素方差分析。
# 使用scipy库的kruskal方法进行方差分析
# https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.kruskal.html
H, p = stats.kruskal(*favorite_test_stages)
if p < 0.05:
print("H值为 {}, 对应P值为 {} < 0.05, 拒绝零假设".format(H, p))
else:
print("H值为 {}, 对应P值为 {} > 0.05, 拒绝零假设失败".format(H, p))
根据上表的结果,我们的自由度为5,得出的H值为84.35263997245896,对应P值为1.0289064814520131e-16,小于我们设置的α值0.05。因此我们推翻零假设,不同分类之间的平均点赞量确实是有统计上显著的区别的。
我们接下来需要解答的两个问题,分别是:1)到底哪些组之间的均值互不相同?2)我们的统计结果能在多大程度上说明问题?
因为我们的样本不具有齐方差性,因此后续检验需要同样非参数性的检验方法。Dunn's Test是Kruskal-Wallis单因素方差分析后续检验的好方法。这是一个逐对比较不同分类均值的检验方法。其针对每一对比较的零假设(H0)是:观察到从样本1中随机抽取的值大于样本2中随机抽取的值的几率为0.5(也即两样本的均值相同)。其对应的备择假设(H1)为:观察到从样本1中随机抽取的值大于样本2中随机抽取的值的几率不为0.5,也即两样本的均值不相同。我们用于执行这项检验的函数会返回一个包含P值的Dataframe,供我们判断有没有对样本对的检验能够成功拒绝零假设。
# 参考链接
# https://scikit-posthocs.readthedocs.io/en/latest/generated/scikit_posthocs.posthoc_dunn/
# https://stats.stackexchange.com/questions/108958/kruskal-wallis-test-is-not-significant-but-some-of-the-mann-whitney-comparisons/108966#108966
result_6200 = sp.posthoc_dunn(df620, 'favorite_log', 'stages', p_adjust=None)
result_6200
result_6200_x = (result_6200 < 0.05) & (result_6200 > 0)
plt.subplots(figsize=(6, 6))
sns.heatmap(result_6200_x, annot=True, fmt="d", linewidths=.5,
cbar=False, cmap=["#374649", "#01B8AA"], square=True)
# 设置大图标题
plt.suptitle('Unique Pairs Highlighted', fontweight='bold', y=.93)
利用Dunn's检验,我们发现,除doggo & pupper组与其他分类没有区别外,其他每个组都与其他组有区别。但观察fig620开支,其实这些不同的组就是分成了两组:doggo, puppo和floofer一组,not specified和pupper一组,混合了doggo还有pupper的属于墙头草。
这就意味着这么一个有趣的事实:不考虑推文质量等因素,doggo, puppo和floofer的吸赞能力一致;令人大跌眼镜的是,一向是卖萌担当的小狗,竟和推主没有说明分类时的吸赞能力一致,而混合了大狗和小狗的图片的吸赞能力则介于二者之间,统计学上没有证据证明其吸赞能力与上述两组有差。
fig620
η2是反映组间变异占整体变异的比例的效应量。针对Kruskal-Wallis检验的η2计算方法如下:
$$ η^2 = χ^2/N-1 $$
SciPy并没有直接报告χ2的值,但因我们的样本容量足够(全部大于5)其给出的H值可以被当作χ2。N是所有分组样本之和,是1991。η2计算如下:
effect_size_6200 = H/(1991-1)
print("η^2 = {}".format(effect_size_6200))
我们针对不同分类之间点赞量的不同进行的方差分析,虽然说明了不同组之间的点赞量确实存在具有统计学意义的差别,但其效应量只有0.0423,比较小,对整体结果没有很强的效果。
RTLikeRatio_test_stages = [df620[df620['stages'] == 'Not Specified']['RL_Ratio_log'],
df620[df620['stages'] == 'doggo']['RL_Ratio_log'],
df620[df620['stages'] == 'puppo']['RL_Ratio_log'],
df620[df620['stages'] == 'pupper']['RL_Ratio_log'],
df620[df620['stages'] == 'floofer']['RL_Ratio_log'],
df620[df620['stages'] == 'doggo & pupper']['RL_Ratio_log']]
# 参考链接 https://pythonfordatascience.org/anova-python/
s, p = stats.levene(*RTLikeRatio_test_stages)
if p < 0.05:
print('应拒绝零假设,样本不具方差齐性')
else:
print('拒绝零假设失败, 继续')
# 使用statsmodel进行方差分析
# formula语法: http://www.statsmodels.org/stable/contrasts.html
# 模型拟合
# http://www.statsmodels.org/stable/example_formulas.html,
# https://mcfromnz.wordpress.com/2011/03/02/anova-type-iiiiii-ss-explained/
# 其他:
# https://www.marsja.se/four-ways-to-conduct-one-way-anovas-using-python/
# https://pythonfordatascience.org/anova-python/
# https://www.statsmodels.org/stable/generated/statsmodels.stats.anova.anova_lm.html#statsmodels.stats.anova.anova_lm
mod6201 = ols(formula="RL_Ratio_log ~ C(stages)", data=df620).fit()
anova_table_6201 = sm.stats.anova_lm(mod6201, typ=2)
anova_table_6201
根据上表的结果,F(5, 1985) = 2.974431,对应P值为0.011124,小于我们设置的α值0.05。因此我们推翻零假设,不同分类之间的转赞比均值确实是有统计上显著的区别的。
我们接下来需要解答的两个问题,分别是:1)到底哪些组之间的均值互不相同?2)我们的统计结果能在多大程度上说明问题?
# 参考链接
# https://www.statsmodels.org/dev/generated/statsmodels.sandbox.stats.multicomp.MultiComparison.tukeyhsd.html
# https://www.statsmodels.org/dev/generated/statsmodels.stats.multicomp.pairwise_tukeyhsd.html
# http://cleverowl.uk/2015/07/01/using-one-way-anova-and-tukeys-test-to-compare-data-sets/
mc6201 = MultiComparison(df620['RL_Ratio_log'], df620['stages'])
hsd_result = mc6201.tukeyhsd()
print(hsd_result, '\n\n', "Unique Groups: {}".format(mc6201.groupsunique))
利用Tukey的HSD检验,我们发现,除了pupper和puppo这两组的均值显著的不同之外,其他所有的组两两之间都一样。
η2是反映组间变异占整体变异的比例的效应量。针对不同分类之间的转赞比的区别的η2计算结果如下:
effect_size_6201 = anova_table_6201['sum_sq'][0] / \
(anova_table_6201['sum_sq'][0] + anova_table_6201['sum_sq'][1])
print("η^2 = {}".format(effect_size_6201))
针对不同分类之间转赞比的不同进行的方差分析虽然在统计上说明了不同组之间的转赞比确实存在具有统计学意义的差别,但其效应量只有0.007,很小,在实际当中可能根本不显著。
尽管从直觉出发,我们会认为小狗更受欢迎,不同的分类之间可能有不同的受众,但统计检验的结果并没有完全支持我们猜想。
点赞量上,我们发现, 只有doggo & pupper这一大狗带小狗的组合的访问量的均值与组的均值没有统计学意义上显著的差别;而剩下的分类,则分为了两个互不相同的大组:doggo, puppo和floofer一组,未说明的推文和属于pupper的推文一组。而未说明的推文和pupper分类的推文,点赞量较另一大组低。这个结论实际上否定了我们的猜想:小狗并没有更受欢迎。
至于通过转赞比探索不同的组之间是否有不同的受众,尽管我们得出了puppo和pupper的转赞比均值有统计学意义上显著的差别,其效应量也很小。因此,我们不认为不同的分类之间有实际意义上的不同受众群体, 而poppo和pupper推文反映出的点赞比的差别很可能是由于其他我们没有考察的因素导致的。
fig630
fig631
fig632
# 建立备用数据集,并设定截距
df6300 = df620.copy()
df6300['intercept'] = 1
df6300.head(1)
plt.subplots(figsize=(9, 9))
sns.regplot(x=dogrates_clean['average'], y=dogrates_clean['favorite_count'].apply(
np.log10), color='#01B8AA')
# 设置大图标题
plt.suptitle('Favorites Scatter & Regression for Average Ratings',
fontweight='bold', y=.93)
model6300 = sm.OLS(df6300['favorite_log'], df6300[['intercept', 'average']])
results6300 = model6300.fit()
results6300.summary()
所以,点赞量和平均分之间的函数关系为:
favorite_count = 100.1286*average+2.2742
这一模型能够覆盖24.9%的数据。
plt.subplots(figsize=(9, 9))
sns.regplot(x=dogrates_clean['average'],
y=dogrates_clean['RT/Like'].apply(np.log), color='#374649')
# 设置大图标题
plt.suptitle('RT/Like Ratio Scatter & Regression for Average Ratings',
fontweight='bold', y=.93)
model6301 = sm.OLS(df6300['RL_Ratio_log'], df6300[['intercept', 'average']])
results6301 = model6301.fit()
results6301.summary()
转赞比与平均分的函数关系为:
RT/Like = e-0.0514*average-0.6176
不过,这一模型只适用于大约11.9%的数据,不是很有效。
favorite_test_avgcate = [df620[df620['average_cate'] == 'very_low']['favorite_log'],
df620[df620['average_cate'] == 'low']['favorite_log'],
df620[df620['average_cate'] ==
'medium']['favorite_log'],
df620[df620['average_cate'] == 'high']['favorite_log']]
# 使用levene test检验样本的方差齐性
s, p = stats.levene(*favorite_test_avgcate)
if p < 0.05:
print('应拒绝零假设,样本不具方差齐性,需要使用Kruskal-Wallis单因素方差分析')
else:
print('拒绝零假设失败, 继续')
# 使用statsmodel进行方差分析
mod6301 = ols(formula="favorite_log ~ C(average_cate)", data=df620).fit()
anova_table_6301 = sm.stats.anova_lm(mod6301, typ=2)
anova_table_6301
# 使用Tukey's HSD找出真正不同的组
mc6301 = MultiComparison(df620['favorite_log'], df620['average_cate'])
hsd_result = mc6301.tukeyhsd()
print(hsd_result, '\n\n', "Unique Groups: {}".format(mc6301.groupsunique))
利用Tukey的HSD检验,我们发现:在点赞量上,最高分组与非常低,低和中等三个小组都显著的不同;而三小组之间的差别则没有统计学意义上显著的不同。
η2是反映组间变异占整体变异的比例的效应量。针对不同分类之间的转赞比的区别的η2计算结果如下:
effect_size_6301 = anova_table_6301['sum_sq'][0] / \
(anova_table_6301['sum_sq'][0] + anova_table_6301['sum_sq'][1])
print("η^2 = {}".format(effect_size_6301))
我们发现,针对点赞量的单因素方差分析能够解释26%的点赞变化。不算特别显著,但至少我们能够认为,分数的高低确实能够作为影响点赞量的一个因素:分数超过10分,将为推文带来分数低于10分在本质上不同的点赞量。
RTLikeRatio_test_avgcate = [df620[df620['average_cate'] == 'very_low']['RL_Ratio_log'],
df620[df620['average_cate']
== 'low']['RL_Ratio_log'],
df620[df620['average_cate'] ==
'medium']['RL_Ratio_log'],
df620[df620['average_cate'] == 'high']['RL_Ratio_log']]
# 使用levene test检验样本的方差齐性
s, p = stats.levene(*RTLikeRatio_test_avgcate)
if p < 0.05:
print('应拒绝零假设,样本不具方差齐性,需要使用Kruskal-Wallis单因素方差分析')
else:
print('拒绝零假设失败, 继续')
# 使用scipy库的kruskal方法进行方差分析
# https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.kruskal.html
H6301, p = stats.kruskal(*RTLikeRatio_test_avgcate)
if p < 0.05:
print("H值为 {}, 对应P值为 {} < 0.05, 拒绝零假设".format(H, p))
else:
print("H值为 {}, 对应P值为 {} > 0.05, 拒绝零假设失败".format(H, p))
# 使用Dunn检验找出独特的组
result_6301 = sp.posthoc_dunn(
df620, 'RL_Ratio_log', 'average_cate', p_adjust=None)
result_6301
result_6301_x = (result_6301 < 0.05) & (result_6301 > 0)
plt.subplots(figsize=(4, 4))
sns.heatmap(result_6301_x, annot=True, fmt="d", linewidths=.5,
cbar=False, cmap=["#374649", "#01B8AA"], square=True)
# 设置大图标题
plt.suptitle('Unique Pairs Highlighted', fontweight='bold', y=.95)
根据Dunn's检验的结果,我们发现,最高分数的一组的转赞比低于其他三组,而中间的转赞比则低于分数低和非常低的两组。也就是说,分数越低,转赞比确实越高。有意思。
fig631
effect_size_6301 = H6301/(1991-1)
print("η^2 = {}".format(effect_size_6301))
统计结果大约能解释10%的转赞比变化情况,这一比例较低。因此,转暂避的变化虽然跟分数的到底确实有关系,但关系不算显著。
尽管我们成功归纳出了点赞量与转赞比与平均分数之间的函数关系,但其并不能很好的说明这两个变量随着评分变化情况的变化。
为了进一步探索这一现象,我们使用分数段对所有的推文进行了分组并执行了单因素方差分析。
对点赞量的分析结果显示,分数高于10分,能够吸引比分数低于10分更多的点赞量,而低分数低于10分的推文的点赞量,无论深处哪个分数段,并没有统计学意义上的不同。
而对转赞比的分析显示,分数低于7分的推文的转赞比,较分数在7-10分,和10分以上推文的转赞比有统计学意义上的不同。
尽管以上两点观察效应量都很低,但这确实是线性回归没有也不能告诉我们的。
在经历如此冗长的清洗,可视化,分析,计算和讨论之后,我们终于来到了这个环节~
我们将首先回顾我们对原始数据集所作的清理,这项清理工作奠定了我们分析工作的基础;其次,我们将回顾我们对推特账号运营情况的观察,这将有助于我们了解我们分析的对象的整体状况,并作为后续高级分析的基础;最后,我们专注于解答三个问题:是否有某些品种的狗狗更受欢迎?是否有某些生长阶段的狗狗更受欢迎?以及,是否有某些特定分数段的狗狗更受欢迎?最后,我们将讨论这份报告的局限性,以及对此数据集的后续挖掘可能展开的方向。
我们花费了大约1/2的篇幅用于观察和清理这个数据集;这个工作无疑是这个项目的重中之重。
针对数据集存在的质量问题,我们进行了如下操作:
timestamp列数据类型错误的问题:将其修正为datatime数据类型;针对数据集的整洁度问题,我们进行了如下操作:
在以上工作完成之后,我们确实得到了一个清洗干净的数据集,但它还不足以解答我们的问题:有没有因素能帮助我们确定,这条推文会更受欢迎?为此,我们在上述工作的基础之上,进一步提取了如下信息,以帮助我们进一步探索数据集:
尽管还有许多方面值得进一步挖掘,但我们针对这个数据集的清洗和整理工作就到这里。
在探索的过程中,我们发现了这些有趣的现象:
在这份报告中,我们探索了7种可能对受欢迎程度有影响的因素:客观因素有时间,星期;主观因素有品种,生长阶段分类,评分与评分分组,推文主体是不是狗,以及推文主体的性别因素。在初步的可视化探索之后,我们认为只有品种,生长阶段分类,和评分与评分分组这三个方值得进一步探索;客观因素上看不出有明显的规律,而且变量太多;其他主观因素波动量很小,就算我们进行进一步探索,就算我们得出统计上显著的结论,其效应量也很可能很小,不具实际意义。
我们最终得出的结论可以由以下三句话来概括:
sns.set(style='whitegrid')
fig610 = sns.PairGrid(pivot_breeds_plotting_enhanced, palette=ui_palette_light,
x_vars=[('favorite_count', 'mean'), ('retweet_count', 'mean'), ('RT/Like', 'mean'),
('tweet_id', 'len')], y_vars=['breed'], height=8, aspect=.35)
fig610 = fig610.map(sns.barplot, orient='h', edgecolor='w',
palette=ui_palette_light)
# 设定标题,轴等
titles = ['Favorites Avg.', 'Retweets Avg.',
'RT/Like Ratio', 'Number of Tweets']
for ax, title in zip(fig610.axes.flat, titles):
# 为每个轴单独设定标题
ax.set(title=title)
# 取消纵向网格线,改为水平网格线
ax.xaxis.grid(False)
ax.yaxis.grid(True)
# 设置大图标题
plt.suptitle('Details of Most Favored Breeds', fontweight='bold', y=1.04)
我们筛选出了单条推特平均点赞量前10的狗狗。看上去每条狗狗的在不同的指标上差距都不小。
为了进一步确认这些狗狗的平均点赞量和转赞比有没有统计学意义上的不同,我们分布对这两项指标进行了单因素方差分析。结果显示,这10各品种狗狗的平均点赞量和转赞比没有统计学意义上的差别。这也意味着,尽管有些狗狗收获的单条点赞量较多,但我们不能据此认为某些狗狗更受欢迎。更合理的解释是,在可爱又调皮的狗狗面前,不管什么品种大家都爱,正所谓:大千世界,众汪平等。
fig620
通过对不同生长阶段的分类的探索,我们发现,小狗狗pupper的平均点赞量低,居然时真的和其他的分类有统计学意义上的不同的。当推主没有说明狗狗的生长状态的时候,其吸赞能力和小狗pupper是一个水平;而其他的分类,如大狗doggo,青少年狗puppo,和毛好看狗floofer的吸赞能力在二者之上。大狗带小狗的分类的吸赞能力在这二者之间。
而对转赞比的分析,则显示推特账号的粉丝似乎并没有特别强烈的偏好,唯一在统计学意义上不同的组就是puppo和pupper,且这个统计的效应量只能解释0.743%的变化.在实际中,我们可以认为粉丝们对不同阶段的狗狗是一视同仁的。
至于为什么小狗狗反而不吸睛呢?这有可能是因为本公众号的核心优势在于搞笑,而小狗的“萌”属性并没有很好的与搞笑结合,导致这一,也有可能是因为有小狗的特征没有提取出来
针对评分与点赞量/转发量之间的关系问题,我们进行了线性回归和单因素方差分析两种探索。两个模型单独都不能很好的解释发生的现象(虽然都是这篇报告中最好的),但将二者合起来看,会带来有趣的见解。
fig631
回归模型为我们带来了分数与点赞量和转赞比关系的公式:
favorite_count = 10^0.1286*average+2.2742;
RT/Like = e^-0.0514*average-0.6176
点赞量随着分数的提高而提高,转赞比却随着分数的提高而不断下滑。但是,针对点赞的回归模型只能解释24.9%的数据,而针对转赞比的回归模型则只能解释11.9%的数据。效果不算好。仔细观察图片发现,点赞量的回归方程似乎只适用于分数在10分以上的数据;而转赞比的方程也将大量的低分抛弃在外。
fig632
针对不同分数段进行的单因素方差分析显示:
点赞量上,分数高于10分,将为推文带来与低于10分显著不同的点赞量,而低于10分的三组自身没有统计学意义上显著的差异;转赞比上,低于7分的低分和超低分组拥有更高的转赞比,而高分组的转赞比则统计学意义上显著的最低。
这很可能是由于低分组的推特集中在推特账号草创时期,也即转赞比很高的时期发出,因为拥有更低的点赞量和更高的转赞比。(见下面二图)
of.iplot(fig4523)
fig4532
目前能想到的局限性:
目前能想到的,可以进一步挖掘的方向: